From 5148ed2b82fd591766d217fdf8507b72fbb2167b Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 4 Sep 2025 08:04:54 -0400 Subject: [PATCH] refactor(tools): migrate challenge editor to new curriculum structure (#61968) --- tools/challenge-editor/api/configs/paths.ts | 33 +++++++---- .../api/configs/super-block-list.ts | 56 +++++++++--------- .../api/interfaces/superblock-meta.ts | 14 +++++ .../challenge-editor/api/utils/get-blocks.ts | 56 ++++++++++++------ .../api/utils/get-full-stack-blocks.ts | 58 +++++++++++++++++++ .../api/utils/get-step-contents.ts | 2 +- tools/challenge-editor/api/utils/get-steps.ts | 28 ++++++--- tools/challenge-editor/api/utils/save-step.ts | 2 +- .../client/src/components/block/block.tsx | 20 +++---- .../src/components/superblock/super-block.tsx | 2 +- .../client/src/utils/block-name-translator.ts | 53 +++++++++-------- 11 files changed, 217 insertions(+), 107 deletions(-) create mode 100644 tools/challenge-editor/api/interfaces/superblock-meta.ts create mode 100644 tools/challenge-editor/api/utils/get-full-stack-blocks.ts diff --git a/tools/challenge-editor/api/configs/paths.ts b/tools/challenge-editor/api/configs/paths.ts index 26b1ef28f1a..b81fc8beaf2 100644 --- a/tools/challenge-editor/api/configs/paths.ts +++ b/tools/challenge-editor/api/configs/paths.ts @@ -1,5 +1,25 @@ import { join } from 'path'; +export const SUPERBLOCK_META_DIR = join( + process.cwd(), + '..', + '..', + '..', + 'curriculum', + 'structure', + 'superblocks' +); + +export const BLOCK_META_DIR = join( + process.cwd(), + '..', + '..', + '..', + 'curriculum', + 'structure', + 'blocks' +); + export const CHALLENGE_DIR = join( process.cwd(), '..', @@ -7,15 +27,6 @@ export const CHALLENGE_DIR = join( '..', 'curriculum', 'challenges', - 'english' -); - -export const META_DIR = join( - process.cwd(), - '..', - '..', - '..', - 'curriculum', - 'challenges', - '_meta' + 'english', + 'blocks' ); diff --git a/tools/challenge-editor/api/configs/super-block-list.ts b/tools/challenge-editor/api/configs/super-block-list.ts index 714d83228cf..f56f717d039 100644 --- a/tools/challenge-editor/api/configs/super-block-list.ts +++ b/tools/challenge-editor/api/configs/super-block-list.ts @@ -1,114 +1,114 @@ export const superBlockList = [ { name: 'Legacy Responsive Web Design', - path: '01-responsive-web-design' + path: 'responsive-web-design' }, { name: 'Legacy JavaScript Algorithms and Data Structures', - path: '02-javascript-algorithms-and-data-structures' + path: 'javascript-algorithms-and-data-structures' }, { name: 'Front End Development Libraries', - path: '03-front-end-development-libraries' + path: 'front-end-development-libraries' }, { name: 'Data Visualization', - path: '04-data-visualization' + path: 'data-visualization' }, { name: 'Back End Development and APIs', - path: '05-back-end-development-and-apis' + path: 'back-end-development-and-apis' }, { name: 'Quality Assurance', - path: '06-quality-assurance' + path: 'quality-assurance' }, { name: 'Scientific Computing with Python', - path: '07-scientific-computing-with-python' + path: 'scientific-computing-with-python' }, { name: 'Data Analysis with Python', - path: '08-data-analysis-with-python' + path: 'data-analysis-with-python' }, { name: 'Information Security', - path: '09-information-security' + path: 'information-security' }, { name: 'Coding Interview Prep', - path: '10-coding-interview-prep' + path: 'coding-interview-prep' }, { name: 'Machine Learning with Python', - path: '11-machine-learning-with-python' + path: 'machine-learning-with-python' }, { name: 'Relational Databases', - path: '13-relational-databases' + path: 'relational-databases' }, { name: 'Responsive Web Design', - path: '14-responsive-web-design-22' + path: 'responsive-web-design-22' }, { name: 'JavaScript Algorithms and Data Structures', - path: '15-javascript-algorithms-and-data-structures-22' + path: 'javascript-algorithms-and-data-structures-22' }, { name: 'The Odin Project', - path: '16-the-odin-project' + path: 'the-odin-project' }, { name: 'College Algebra with Python', - path: '17-college-algebra-with-python' + path: 'college-algebra-with-python' }, { name: 'Project Euler', - path: '18-project-euler' + path: 'project-euler' }, { name: '(New) Foundational C# with Microsoft', - path: '19-foundational-c-sharp-with-microsoft' + path: 'foundational-c-sharp-with-microsoft' }, { name: 'A2 English for Developers (Beta)', - path: '21-a2-english-for-developers' + path: 'a2-english-for-developers' }, { name: 'Rosetta Code', - path: '22-rosetta-code' + path: 'rosetta-code' }, { name: 'Python For Everybody', - path: '23-python-for-everybody' + path: 'python-for-everybody' }, { name: 'B1 English for Developers (Beta)', - path: '24-b1-english-for-developers' + path: 'b1-english-for-developers' }, { name: 'Certified Full Stack Developer', - path: '25-front-end-development' + path: 'full-stack-developer' }, { name: 'A2 Professional Spanish (Beta)', - path: '26-a2-professional-spanish' + path: 'a2-professional-spanish' }, { name: 'A2 Professional Chinese (Beta)', - path: '27-a2-professional-chinese' + path: 'a2-professional-chinese' }, { name: 'Basic HTML', - path: '28-basic-html' + path: 'basic-html' }, { name: 'Semantic HTML', - path: '29-semantic-html' + path: 'semantic-html' }, { name: 'A1 Professional Chinese (Beta)', - path: '30-a1-professional-chinese' + path: 'a1-professional-chinese' } ]; diff --git a/tools/challenge-editor/api/interfaces/superblock-meta.ts b/tools/challenge-editor/api/interfaces/superblock-meta.ts new file mode 100644 index 00000000000..7a93317e6e3 --- /dev/null +++ b/tools/challenge-editor/api/interfaces/superblock-meta.ts @@ -0,0 +1,14 @@ +export interface SuperBlockModule { + dashedName: string; + blocks?: string[]; +} + +export interface SuperBlockChapter { + dashedName: string; + modules: SuperBlockModule[]; +} + +export interface SuperBlockMeta { + blocks?: string[]; + chapters?: SuperBlockChapter[]; +} diff --git a/tools/challenge-editor/api/utils/get-blocks.ts b/tools/challenge-editor/api/utils/get-blocks.ts index 71d76dd2342..c18a11f8ec9 100644 --- a/tools/challenge-editor/api/utils/get-blocks.ts +++ b/tools/challenge-editor/api/utils/get-blocks.ts @@ -1,8 +1,8 @@ -import { readdir, readFile } from 'fs/promises'; +import { readFile } from 'fs/promises'; import { join } from 'path'; -import { CHALLENGE_DIR, META_DIR } from '../configs/paths'; +import { SUPERBLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths'; -import { PartialMeta } from '../interfaces/partial-meta'; +import { SuperBlockMeta } from '../interfaces/superblock-meta'; type Block = { name: string; @@ -10,23 +10,41 @@ type Block = { }; export const getBlocks = async (sup: string): Promise => { - const filePath = join(CHALLENGE_DIR, sup); + const superBlockDataPath = join(SUPERBLOCK_META_DIR, sup + '.json'); + const superBlockMetaFile = await readFile(superBlockDataPath, { + encoding: 'utf8' + }); + const superBlockMeta = JSON.parse(superBlockMetaFile) as SuperBlockMeta; + let blocks: { name: string; path: string }[] = []; - const files = await readdir(filePath); - const blocks = await Promise.all( - files.map(async file => { - const metaPath = join(META_DIR, file, 'meta.json'); - - const metaData = JSON.parse( - await readFile(metaPath, 'utf8') - ) as PartialMeta; - - return { - name: metaData.name, - path: file - }; - }) - ); + if (sup === 'full-stack-developer') { + const moduleBlockData = await Promise.all( + superBlockMeta.chapters!.flatMap(async chapter => { + return await Promise.all( + chapter.modules.flatMap(async module => { + return module.blocks!.flatMap(block => { + const filePath = join(CHALLENGE_DIR, block); + return { + name: block, + path: filePath + }; + }); + }) + ); + }) + ); + blocks = moduleBlockData.flat().flat(); + } else { + blocks = await Promise.all( + superBlockMeta.blocks!.map(async block => { + const filePath = join(CHALLENGE_DIR, block); + return { + name: block, + path: filePath + }; + }) + ); + } return blocks; }; diff --git a/tools/challenge-editor/api/utils/get-full-stack-blocks.ts b/tools/challenge-editor/api/utils/get-full-stack-blocks.ts new file mode 100644 index 00000000000..135f5b31f39 --- /dev/null +++ b/tools/challenge-editor/api/utils/get-full-stack-blocks.ts @@ -0,0 +1,58 @@ +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import { SUPERBLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths'; +import { SuperBlockMeta } from '../interfaces/superblock-meta'; + +type Block = { + name: string; + path: string; +}; + +export const getModules = async (chap: string): Promise => { + const superBlockDataPath = join( + SUPERBLOCK_META_DIR, + 'full-stack-developer' + '.json' + ); + + const superBlockMetaFile = await readFile(superBlockDataPath, { + encoding: 'utf8' + }); + const superBlockMeta = JSON.parse(superBlockMetaFile) as SuperBlockMeta; + + const chapter = superBlockMeta.chapters!.filter( + x => x.dashedName === chap + )[0]; + + return await Promise.all( + chapter.modules!.map(async module => module.dashedName) + ); +}; + +export const getBlocks = async (module: string): Promise => { + const superBlockDataPath = join( + SUPERBLOCK_META_DIR, + 'full-stack-developer' + '.json' + ); + + const superBlockMetaFile = await readFile(superBlockDataPath, { + encoding: 'utf8' + }); + const superBlockMeta = JSON.parse(superBlockMetaFile) as SuperBlockMeta; + + const foundModule = superBlockMeta + .chapters!.flatMap(x => x.modules) + .filter(x => x.dashedName === module)[0]; + + let blocks: { name: string; path: string }[] = []; + + blocks = await Promise.all( + foundModule.blocks!.map(async block => { + const filePath = join(CHALLENGE_DIR, block); + return { + name: block, + path: filePath + }; + }) + ); + return blocks; +}; diff --git a/tools/challenge-editor/api/utils/get-step-contents.ts b/tools/challenge-editor/api/utils/get-step-contents.ts index 01452719223..dd71a2627fb 100644 --- a/tools/challenge-editor/api/utils/get-step-contents.ts +++ b/tools/challenge-editor/api/utils/get-step-contents.ts @@ -8,7 +8,7 @@ export const getStepContent = async ( block: string, step: string ): Promise<{ name: string; dashedName: string; fileData: string }> => { - const filePath = join(CHALLENGE_DIR, sup, block, step); + const filePath = join(CHALLENGE_DIR, block, step); const fileData = await readFile(filePath, 'utf8'); const name = matter(fileData).data.title as string; diff --git a/tools/challenge-editor/api/utils/get-steps.ts b/tools/challenge-editor/api/utils/get-steps.ts index 943056a461b..99facc0b3ea 100644 --- a/tools/challenge-editor/api/utils/get-steps.ts +++ b/tools/challenge-editor/api/utils/get-steps.ts @@ -1,10 +1,10 @@ -import { readdir, readFile } from 'fs/promises'; +import { readFile } from 'fs/promises'; import { join } from 'path'; import matter from 'gray-matter'; import { PartialMeta } from '../interfaces/partial-meta'; -import { CHALLENGE_DIR, META_DIR } from '../configs/paths'; +import { BLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths'; const getFileOrder = (id: string, meta: PartialMeta) => { return meta.challengeOrder.findIndex(({ id: f }) => f === id); @@ -17,16 +17,25 @@ type Step = { }; export const getSteps = async (sup: string, block: string): Promise => { - const filePath = join(CHALLENGE_DIR, sup, block); + //const superMetaPath = join(SUPERBLOCK_META_DIR, sup + ".json"); - const metaPath = join(META_DIR, block, 'meta.json'); + //const superMetaData = JSON.parse( + // await readFile(superMetaPath, 'utf8') + //) as Partial; - const metaData = JSON.parse(await readFile(metaPath, 'utf8')) as PartialMeta; + const stepDirectory = join(CHALLENGE_DIR, block); + + const blockFolderPath = join(BLOCK_META_DIR, block + '.json'); + + const blockMetaData = JSON.parse( + await readFile(blockFolderPath, { encoding: 'utf8' }) + ) as PartialMeta; + + const stepFileNames = blockMetaData.challengeOrder.map(x => x.id + '.md'); - const stepFilenames = await readdir(filePath); const stepData = await Promise.all( - stepFilenames.map(async filename => { - const stepPath = join(filePath, filename); + stepFileNames.map(async filename => { + const stepPath = join(stepDirectory, filename); const step = await readFile(stepPath, 'utf8'); const frontMatter = matter(step); @@ -39,6 +48,7 @@ export const getSteps = async (sup: string, block: string): Promise => { ); return stepData.sort( - (a, b) => getFileOrder(a.id, metaData) - getFileOrder(b.id, metaData) + (a, b) => + getFileOrder(a.id, blockMetaData) - getFileOrder(b.id, blockMetaData) ); }; diff --git a/tools/challenge-editor/api/utils/save-step.ts b/tools/challenge-editor/api/utils/save-step.ts index 5dfb5d3f27a..f8333a9ecbe 100644 --- a/tools/challenge-editor/api/utils/save-step.ts +++ b/tools/challenge-editor/api/utils/save-step.ts @@ -9,7 +9,7 @@ export const saveStep = async ( content: string ): Promise => { try { - const filePath = join(CHALLENGE_DIR, sup, block, step); + const filePath = join(CHALLENGE_DIR, block, step); await writeFile(filePath, content); diff --git a/tools/challenge-editor/client/src/components/block/block.tsx b/tools/challenge-editor/client/src/components/block/block.tsx index 80d3b02cdc8..774739078a7 100644 --- a/tools/challenge-editor/client/src/components/block/block.tsx +++ b/tools/challenge-editor/client/src/components/block/block.tsx @@ -5,18 +5,18 @@ import { API_LOCATION } from '../../utils/handle-request'; import './block.css'; const stepBasedSuperblocks = [ - '07-scientific-computing-with-python', - '14-responsive-web-design-22', - '15-javascript-algorithms-and-data-structures-22', - '25-front-end-development' + 'scientific-computing-with-python', + 'responsive-web-design-22', + 'javascript-algorithms-and-data-structures-22', + 'front-end-development' ]; const taskBasedSuperblocks = [ - '21-a2-english-for-developers', - '24-b1-english-for-developers', - '26-a2-professional-spanish', - '27-a2-professional-chinese', - '30-a1-professional-chinese' + 'a2-english-for-developers', + 'b1-english-for-developers', + 'a2-professional-spanish', + 'a2-professional-chinese', + 'a1-professional-chinese' ]; const Block = () => { @@ -95,7 +95,7 @@ const Block = () => { Looking to add or remove challenges? Navigate to
freeCodeCamp/curriculum/challenges/english - {`/${params.superblock}/${params.block}/`} + {`/${params.block}/`}
in your terminal and run the following commands: diff --git a/tools/challenge-editor/client/src/components/superblock/super-block.tsx b/tools/challenge-editor/client/src/components/superblock/super-block.tsx index 009b96b1dae..676cec33e7e 100644 --- a/tools/challenge-editor/client/src/components/superblock/super-block.tsx +++ b/tools/challenge-editor/client/src/components/superblock/super-block.tsx @@ -42,7 +42,7 @@ const SuperBlock = () => {
    {items.map(block => (
  • - {block.name} + {block.name}
  • ))}
diff --git a/tools/challenge-editor/client/src/utils/block-name-translator.ts b/tools/challenge-editor/client/src/utils/block-name-translator.ts index dd484568f65..e7fdb7a142b 100644 --- a/tools/challenge-editor/client/src/utils/block-name-translator.ts +++ b/tools/challenge-editor/client/src/utils/block-name-translator.ts @@ -1,31 +1,30 @@ export const superBlockNameMap: { [key: string]: string } = { - '01-responsive-web-design': 'responsive-web-design', - '02-javascript-algorithms-and-data-structures': + 'responsive-web-design': 'responsive-web-design', + 'javascript-algorithms-and-data-structures': 'javascript-algorithms-and-data-structures', - '03-front-end-development-libraries': 'front-end-development-libraries', - '04-data-visualization': 'data-visualization', - '05-back-end-development-and-apis': 'back-end-development-and-apis', - '06-quality-assurance': 'quality-assurance', - '07-scientific-computing-with-python': 'scientific-computing-with-python', - '08-data-analysis-with-python': 'data-analysis-with-python', - '09-information-security': 'information-security', - '10-coding-interview-prep': 'coding-interview-prep', - '11-machine-learning-with-python': 'machine-learning-with-python', - '13-relational-databases': 'relational-databases', - '14-responsive-web-design-22': '2022/responsive-web-design', - '15-javascript-algorithms-and-data-structures-22': + 'front-end-development-libraries': 'front-end-development-libraries', + 'data-visualization': 'data-visualization', + 'back-end-development-and-apis': 'back-end-development-and-apis', + 'quality-assurance': 'quality-assurance', + 'scientific-computing-with-python': 'scientific-computing-with-python', + 'data-analysis-with-python': 'data-analysis-with-python', + 'information-security': 'information-security', + 'coding-interview-prep': 'coding-interview-prep', + 'machine-learning-with-python': 'machine-learning-with-python', + 'relational-databases': 'relational-databases', + 'responsive-web-design-22': '2022/responsive-web-design', + 'javascript-algorithms-and-data-structures-22': 'javascript-algorithms-and-data-structures-v8', - '16-the-odin-project': 'the-odin-project', - '17-college-algebra-with-python': 'college-algebra-with-python', - '18-project-euler': 'project-euler', - '19-foundational-c-sharp-with-microsoft': - 'foundational-c-sharp-with-microsoft', - '21-a2-english-for-developers': 'a2-english-for-developers', - '22-rosetta-code': 'rosetta-code', - '23-python-for-everybody': 'python-for-everybody', - '24-b1-english-for-developers': 'b1-english-for-developers', - '25-front-end-development': 'full-stack-developer', - '26-a2-professional-spanish': 'a2-professional-spanish', - '27-a2-professional-chinese': 'a2-professional-chinese', - '30-a1-professional-chinese': 'a1-professional-chinese' + 'the-odin-project': 'the-odin-project', + 'college-algebra-with-python': 'college-algebra-with-python', + 'project-euler': 'project-euler', + 'foundational-c-sharp-with-microsoft': 'foundational-c-sharp-with-microsoft', + 'a2-english-for-developers': 'a2-english-for-developers', + 'rosetta-code': 'rosetta-code', + 'python-for-everybody': 'python-for-everybody', + 'b1-english-for-developers': 'b1-english-for-developers', + 'front-end-development': 'full-stack-developer', + 'a2-professional-spanish': 'a2-professional-spanish', + 'a2-professional-chinese': 'a2-professional-chinese', + 'a1-professional-chinese': 'a1-professional-chinese' };