chore(tools): display chapters in challenge editor (#62050)

This commit is contained in:
Anna
2025-09-15 10:30:13 -04:00
committed by GitHub
parent eb2c74b4e6
commit 954117ce5e
21 changed files with 622 additions and 58 deletions

View File

@@ -30,3 +30,14 @@ export const CHALLENGE_DIR = join(
'english',
'blocks'
);
export const ENGLISH_LANG_DIR = join(
process.cwd(),
'..',
'..',
'..',
'client',
'i18n',
'locales',
'english'
);

View File

@@ -0,0 +1,11 @@
interface SuperBlock {
title: string;
intro: string[];
blocks: string[];
modules?: string[];
chapters?: string[];
}
export interface Intro {
[key: string]: SuperBlock;
}

View File

@@ -0,0 +1,13 @@
import { Request, Response } from 'express';
import { getBlocks } from '../utils/get-full-stack-blocks';
export const moduleBlockRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, chapter, module } = req.params;
const steps = await getBlocks(superblock, chapter, module);
res.json(steps);
};

View File

@@ -0,0 +1,13 @@
import { Request, Response } from 'express';
import { getModules } from '../utils/get-full-stack-blocks';
export const moduleRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, chapter } = req.params;
const steps = await getModules(superblock, chapter);
res.json(steps);
};

View File

@@ -0,0 +1,9 @@
import { Request, Response } from 'express';
import { getStepContent } from '../utils/get-step-contents';
export const stepRoute = async (req: Request, res: Response): Promise<void> => {
const { superblock, block, step } = req.params;
const stepContents = await getStepContent(superblock, block, step);
res.json(stepContents);
};

View File

@@ -9,6 +9,8 @@ import { saveRoute } from './routes/save-route';
import { stepRoute } from './routes/step-route';
import { superblockRoute } from './routes/super-block-route';
import { toolsRoute } from './routes/tools-route';
import { moduleRoute } from './routes/module-route';
import { moduleBlockRoute } from './routes/module-block-route';
const app = express();
@@ -29,6 +31,14 @@ app.post('/:superblock/:block/:step', (req, res, next) => {
saveRoute(req, res).catch(next);
});
app.get(`/:superblock/chapters/:chapter`, (req, res, next) => {
moduleRoute(req, res).catch(next);
});
app.get(`/:superblock/chapters/:chapter/modules/:module`, (req, res, next) => {
moduleBlockRoute(req, res).catch(next);
});
app.get('/:superblock/:block/:step', (req, res, next) => {
stepRoute(req, res).catch(next);
});

View File

@@ -1,50 +1,69 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { SUPERBLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths';
import {
SUPERBLOCK_META_DIR,
BLOCK_META_DIR,
ENGLISH_LANG_DIR
} from '../configs/paths';
import { SuperBlockMeta } from '../interfaces/superblock-meta';
import { PartialMeta } from '../interfaces/partial-meta';
import { Intro } from '../interfaces/intro';
type Block = {
name: string;
path: string;
};
export const getBlocks = async (sup: string): Promise<Block[]> => {
type BlockLocation = {
blocks: Block[];
currentSuperBlock: string;
};
const chapterBasedSuperBlocks = ['full-stack-developer'];
export const getBlocks = async (sup: string): Promise<BlockLocation> => {
const superBlockDataPath = join(SUPERBLOCK_META_DIR, sup + '.json');
const superBlockMetaFile = await readFile(superBlockDataPath, {
encoding: 'utf8'
});
const superBlockMeta = JSON.parse(superBlockMetaFile) as SuperBlockMeta;
const introDataPath = join(ENGLISH_LANG_DIR, 'intro.json');
const introFile = await readFile(introDataPath, {
encoding: 'utf8'
});
const introData = JSON.parse(introFile) as Intro;
let blocks: { name: string; path: string }[] = [];
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();
if (chapterBasedSuperBlocks.includes(sup)) {
blocks = superBlockMeta.chapters!.map(chapter => {
const chapters = Object.entries(introData[sup]['chapters']!);
const chapterTrueName = chapters.filter(
x => x[0] === chapter.dashedName
)[0][1];
return {
name: chapterTrueName,
path: 'chapters/' + chapter.dashedName
};
});
} else {
blocks = await Promise.all(
superBlockMeta.blocks!.map(async block => {
const filePath = join(CHALLENGE_DIR, block);
const blockStructurePath = join(BLOCK_META_DIR, block + '.json');
const blockMetaFile = await readFile(blockStructurePath, {
encoding: 'utf8'
});
const blockMeta = JSON.parse(blockMetaFile) as PartialMeta;
return {
name: block,
path: filePath
name: blockMeta.name,
path: block
};
})
);
}
return blocks;
return { blocks: blocks, currentSuperBlock: introData[sup].title };
};

View File

@@ -1,58 +1,133 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { SUPERBLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths';
import {
SUPERBLOCK_META_DIR,
BLOCK_META_DIR,
ENGLISH_LANG_DIR
} from '../configs/paths';
import { SuperBlockMeta } from '../interfaces/superblock-meta';
import { PartialMeta } from '../interfaces/partial-meta';
import { Intro } from '../interfaces/intro';
type Block = {
name: string;
path: string;
};
export const getModules = async (chap: string): Promise<string[]> => {
const superBlockDataPath = join(
SUPERBLOCK_META_DIR,
'full-stack-developer' + '.json'
);
type Module = {
name: string;
path: string;
};
type BlockLocation = {
blocks: Block[];
currentModule: string;
currentChapter: string;
};
type ModuleLocation = {
modules: Module[];
currentChapter: string;
currentSuperBlock: string;
};
export const getModules = async (
superBlock: string,
chap: string
): Promise<ModuleLocation> => {
const superBlockDataPath = join(SUPERBLOCK_META_DIR, superBlock + '.json');
const superBlockMetaFile = await readFile(superBlockDataPath, {
encoding: 'utf8'
});
const superBlockMeta = JSON.parse(superBlockMetaFile) as SuperBlockMeta;
const introDataPath = join(ENGLISH_LANG_DIR, 'intro.json');
const introFile = await readFile(introDataPath, {
encoding: 'utf8'
});
const introData = JSON.parse(introFile) as Intro;
const chapters = Object.entries(introData[superBlock]['chapters']!);
const chapter = superBlockMeta.chapters!.filter(
x => x.dashedName === chap
)[0];
return await Promise.all(
chapter.modules!.map(async module => module.dashedName)
const chapterTrueName = chapters.filter(x => x[0] === chap)[0][1];
let modules: Module[] = [];
modules = await Promise.all(
chapter.modules!.map(module => {
const modules = Object.entries(introData[superBlock]['modules']!);
const moduleTrueName = modules.filter(
x => x[0] === module.dashedName
)[0][1];
return { name: moduleTrueName, path: 'modules/' + module.dashedName };
})
);
return {
modules: modules,
currentChapter: chapterTrueName,
currentSuperBlock: introData[superBlock].title
};
};
export const getBlocks = async (module: string): Promise<Block[]> => {
const superBlockDataPath = join(
SUPERBLOCK_META_DIR,
'full-stack-developer' + '.json'
);
export const getBlocks = async (
superBlock: string,
chapterName: string,
moduleName: string
): Promise<BlockLocation> => {
const superBlockDataPath = join(SUPERBLOCK_META_DIR, superBlock + '.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];
const introDataPath = join(ENGLISH_LANG_DIR, 'intro.json');
const introFile = await readFile(introDataPath, {
encoding: 'utf8'
});
const introData = JSON.parse(introFile) as Intro;
const modules = Object.entries(introData[superBlock]['modules']!);
const moduleTrueName = modules.filter(x => x[0] === moduleName)[0][1];
const chapters = Object.entries(introData[superBlock]['chapters']!);
const chapterTrueName = chapters.filter(x => x[0] === chapterName)[0][1];
const foundChapter = superBlockMeta.chapters?.filter(
chapter => chapter.dashedName === chapterName
)[0];
const foundModule = foundChapter?.modules.filter(
module => module.dashedName === moduleName
)[0];
let blocks: { name: string; path: string }[] = [];
blocks = await Promise.all(
foundModule.blocks!.map(async block => {
const filePath = join(CHALLENGE_DIR, block);
foundModule!.blocks!.map(async block => {
const blockStructurePath = join(BLOCK_META_DIR, block + '.json');
const blockMetaFile = await readFile(blockStructurePath, {
encoding: 'utf8'
});
const blockMeta = JSON.parse(blockMetaFile) as PartialMeta;
return {
name: block,
path: filePath
name: blockMeta.name,
path: block
};
})
);
return blocks;
return {
blocks: blocks,
currentModule: moduleTrueName,
currentChapter: chapterTrueName
};
};

View File

@@ -4,7 +4,12 @@ import { join } from 'path';
import matter from 'gray-matter';
import { PartialMeta } from '../interfaces/partial-meta';
import { BLOCK_META_DIR, CHALLENGE_DIR } from '../configs/paths';
import {
BLOCK_META_DIR,
CHALLENGE_DIR,
ENGLISH_LANG_DIR
} from '../configs/paths';
import { Intro } from '../interfaces/intro';
const getFileOrder = (id: string, meta: PartialMeta) => {
return meta.challengeOrder.findIndex(({ id: f }) => f === id);
@@ -16,7 +21,16 @@ type Step = {
path: string;
};
export const getSteps = async (sup: string, block: string): Promise<Step[]> => {
type StepLocation = {
steps: Step[];
currentBlock: string;
currentSuperBlock: string;
};
export const getSteps = async (
sup: string,
block: string
): Promise<StepLocation> => {
//const superMetaPath = join(SUPERBLOCK_META_DIR, sup + ".json");
//const superMetaData = JSON.parse(
@@ -27,6 +41,13 @@ export const getSteps = async (sup: string, block: string): Promise<Step[]> => {
const blockFolderPath = join(BLOCK_META_DIR, block + '.json');
const introDataPath = join(ENGLISH_LANG_DIR, 'intro.json');
const introFile = await readFile(introDataPath, {
encoding: 'utf8'
});
const introData = JSON.parse(introFile) as Intro;
const blockMetaData = JSON.parse(
await readFile(blockFolderPath, { encoding: 'utf8' })
) as PartialMeta;
@@ -47,8 +68,14 @@ export const getSteps = async (sup: string, block: string): Promise<Step[]> => {
})
);
return stepData.sort(
const steps = stepData.sort(
(a, b) =>
getFileOrder(a.id, blockMetaData) - getFileOrder(b.id, blockMetaData)
);
return {
steps: steps,
currentBlock: blockMetaData.name,
currentSuperBlock: introData[sup].title
};
};

View File

@@ -2,3 +2,14 @@ export interface Block {
name: string;
path: string;
}
export interface BlocksWithSuperBlock {
blocks: Block[];
currentSuperBlock: string;
}
export interface BlocksWithModule {
blocks: Block[];
currentModule: string;
currentChapter: string;
}

View File

@@ -3,3 +3,9 @@ export interface ChallengeData {
id: string;
path: string;
}
export interface ChallengeDataWithBlock {
steps: ChallengeData[];
currentBlock: string;
currentSuperBlock: string;
}

View File

@@ -0,0 +1,10 @@
export interface Module {
name: string;
path: string;
}
export interface ChaptersWithLocation {
modules: Module[];
currentSuperBlock: string;
currentChapter: string;
}

View File

@@ -6,6 +6,8 @@ import SuperBlock from './components/superblock/super-block';
import Block from './components/block/block';
import Editor from './components/editor/editor';
import Tools from './components/tools/tools';
import ChapterLanding from './components/chapter/chapter';
import ModuleLanding from './components/module/module';
const App = () => {
return (
@@ -16,6 +18,14 @@ const App = () => {
<Route index element={<Landing />} />
<Route path=':superblock' element={<SuperBlock />} />
<Route path=':superblock/:block' element={<Block />} />
<Route
path=':superblock/chapters/:chapter'
element={<ChapterLanding />}
/>
<Route
path=':superblock/chapters/:chapter/modules/:module'
element={<ModuleLanding />}
/>
<Route path=':superblock/:block/_tools' element={<Tools />} />
<Route path=':superblock/:block/:challenge' element={<Editor />} />
</Routes>

View File

@@ -1,6 +1,9 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ChallengeData } from '../../../interfaces/challenge-data';
import {
ChallengeData,
ChallengeDataWithBlock
} from '../../../interfaces/challenge-data';
import { API_LOCATION } from '../../utils/handle-request';
import './block.css';
@@ -24,6 +27,8 @@ const Block = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([] as ChallengeData[]);
const [blockName, setBlockName] = useState('');
const [superBlockName, setSuperBlockName] = useState('');
const params = useParams() as { superblock: string; block: string };
useEffect(() => {
@@ -34,11 +39,13 @@ const Block = () => {
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${params.superblock}/${params.block}`)
.then(res => res.json() as Promise<ChallengeData[]>)
.then(res => res.json() as Promise<ChallengeDataWithBlock>)
.then(
superblocks => {
setLoading(false);
setItems(superblocks);
setItems(superblocks.steps);
setBlockName(superblocks.currentBlock);
setSuperBlockName(superblocks.currentSuperBlock);
},
(error: Error) => {
setLoading(false);
@@ -64,8 +71,8 @@ const Block = () => {
return (
<div>
<h1>{params.block}</h1>
<span className='breadcrumb'>{params.superblock}</span>
<h1>{blockName}</h1>
<span className='breadcrumb'>{superBlockName}</span>
<ul className='step-grid'>
{items.map((challenge, i) => (
<li key={challenge.name}>

View File

@@ -0,0 +1,3 @@
.step-grid {
column-count: 3;
}

View File

@@ -0,0 +1,177 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import {
ChallengeData,
ChallengeDataWithBlock
} from '../../../interfaces/challenge-data';
import { API_LOCATION } from '../../utils/handle-request';
import './chapter-block.css';
const stepBasedSuperblocks = [
'scientific-computing-with-python',
'responsive-web-design-22',
'javascript-algorithms-and-data-structures-22',
'front-end-development'
];
const taskBasedSuperblocks = [
'a2-english-for-developers',
'b1-english-for-developers',
'a2-professional-spanish',
'a2-professional-chinese',
'a1-professional-chinese'
];
const ChapterBasedBlock = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [blockName, setBlockName] = useState('');
const [superBlockName, setSuperBlockName] = useState('');
const [items, setItems] = useState([] as ChallengeData[]);
const params = useParams() as {
superblock: string;
chapter: string;
module: string;
block: string;
};
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${params.superblock}/${params.block}`)
.then(res => res.json() as Promise<ChallengeDataWithBlock>)
.then(
superblocks => {
setLoading(false);
setItems(superblocks.steps);
setBlockName(superblocks.currentBlock);
setSuperBlockName(superblocks.currentSuperBlock);
},
(error: Error) => {
setLoading(false);
setError(error);
}
);
};
if (error) {
return <div>Error: {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
const isStepBasedSuperblock = stepBasedSuperblocks.includes(
params.superblock
);
const isTaskBasedSuperblock = taskBasedSuperblocks.includes(
params.superblock
);
return (
<div>
<h1>{blockName}</h1>
<span className='breadcrumb'>{superBlockName}</span>
<ul className='step-grid'>
{items.map((challenge, i) => (
<li key={challenge.name}>
{!isStepBasedSuperblock && <span>{`${i + 1}: `}</span>}
<Link
to={`/${params.superblock}/${params.block}/${challenge.path}`}
>
{challenge.name}
</Link>
</li>
))}
</ul>
<p>
<Link to={`/${params.superblock}`}>Return to Blocks</Link>
</p>
<hr />
<h2>Project Controls</h2>
{isStepBasedSuperblock ? (
<p>
Looking to add, remove, or edit steps?{' '}
<Link to={`/${params.superblock}/${params.block}/_tools`}>
Use the step tools.
</Link>
</p>
) : isTaskBasedSuperblock ? (
<>
<p>
Looking to add or remove challenges? Navigate to <br />
<code>
freeCodeCamp/curriculum/challenges/english
{`/${params.block}/`}
</code>
<br />
in your terminal and run the following commands:
</p>
<ul>
<li>
<code>pnpm create-next-task</code>: Create the next task style
challenge in this block
</li>
<li>
<code>pnpm create-next-challenge</code>: Create the next challenge
of a different style in this block
</li>
<li>
<code>pnpm insert-task</code>: Create a new task style challenge
in the middle of this block.
</li>
<li>
<code>pnpm delete-task</code>: Delete a task style challenge in
this block.
</li>
<li>
<code>pnpm reorder-tasks</code>: Rename the tasks to the correct
order.
</li>
</ul>
<p>
Refresh the page after running a command to see the changes
reflected.
</p>
</>
) : (
<>
<p>
Looking to add or remove challenges? Navigate to <br />
<code>
freeCodeCamp/curriculum/challenges/english
{`/${params.superblock}/${params.block}/`}
</code>
<br />
in your terminal and run the following commands:
</p>
<ul>
<li>
<code>pnpm create-next-challenge</code>: Create a new challenge at
the end of this block.
</li>
<li>
<code>pnpm insert-challenge</code>: Create a new challenge in the
middle of this block.
</li>
<li>
<code>pnpm delete-challenge</code>: Delete a challenge in this
block.
</li>
</ul>
<p>
Refresh the page after running a command to see the changes
reflected.
</p>
</>
)}
</div>
);
};
export default ChapterBasedBlock;

View File

@@ -0,0 +1,3 @@
.step-grid {
column-count: 3;
}

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { API_LOCATION } from '../../utils/handle-request';
import { Module, ChaptersWithLocation } from '../../../interfaces/chapter';
const ChapterLanding = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([] as Module[]);
const [chapterName, setChapterName] = useState('');
const [superBlockName, setSuperBlockName] = useState('');
const params = useParams() as { superblock: string; chapter: string };
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${params.superblock}/chapters/${params.chapter}`)
.then(res => res.json() as Promise<ChaptersWithLocation>)
.then(
blockData => {
setLoading(false);
setItems(blockData.modules);
setChapterName(blockData.currentChapter);
setSuperBlockName(blockData.currentSuperBlock);
},
(error: Error) => {
setLoading(false);
setError(error);
}
);
};
if (error) {
return <div>Error: {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{chapterName}</h1>
<ul>
{items.map(chapter => (
<li key={chapter.name}>
<Link
to={`/${params.superblock}/chapters/${params.chapter}/${chapter.path}`}
>
{chapter.name}
</Link>
</li>
))}
</ul>
<p>
<Link to={`/${params.superblock}`}>Return to {superBlockName}</Link>
</p>
<hr />
<h2>Create New Project</h2>
<p>
Want to create a new project? Open your terminal and run{' '}
<code>pnpm run create-new-project</code>
</p>
</div>
);
};
export default ChapterLanding;

View File

@@ -0,0 +1,3 @@
.step-grid {
column-count: 3;
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { API_LOCATION } from '../../utils/handle-request';
import { Block, BlocksWithModule } from '../../../interfaces/block';
const ModuleLanding = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([] as Block[]);
const [moduleName, setModuleName] = useState('');
const [chapterName, setChapterName] = useState('');
const params = useParams() as {
superblock: string;
chapter: string;
module: string;
};
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchData = () => {
setLoading(true);
fetch(
`${API_LOCATION}/${params.superblock}/chapters/${params.chapter}/modules/${params.module}`
)
.then(res => res.json() as Promise<BlocksWithModule>)
.then(
moduleData => {
setLoading(false);
setItems(moduleData.blocks);
setModuleName(moduleData.currentModule);
setChapterName(moduleData.currentChapter);
},
(error: Error) => {
setLoading(false);
setError(error);
}
);
};
if (error) {
return <div>Error: {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{moduleName}</h1>
<ul>
{items.map(block => (
<li key={block.path}>
<Link to={`/${params.superblock}/${block.path}`}>{block.name}</Link>
</li>
))}
</ul>
<p>
<Link to={`/${params.superblock}/chapters/${params.chapter}`}>
Return to {chapterName}
</Link>
</p>
<hr />
<h2>Create New Project</h2>
<p>
Want to create a new project? Open your terminal and run{' '}
<code>pnpm run create-new-project</code>
</p>
</div>
);
};
export default ModuleLanding;

View File

@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Block } from '../../../interfaces/block';
import { Block, BlocksWithSuperBlock } from '../../../interfaces/block';
import { API_LOCATION } from '../../utils/handle-request';
const SuperBlock = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([] as Block[]);
const [superBlockName, setSuperBlockName] = useState('');
const params = useParams() as { superblock: string; block: string };
useEffect(() => {
@@ -17,11 +18,12 @@ const SuperBlock = () => {
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${params.superblock}`)
.then(res => res.json() as Promise<Block[]>)
.then(res => res.json() as Promise<BlocksWithSuperBlock>)
.then(
blocks => {
blockData => {
setLoading(false);
setItems(blocks);
setItems(blockData.blocks);
setSuperBlockName(blockData.currentSuperBlock);
},
(error: Error) => {
setLoading(false);
@@ -38,11 +40,11 @@ const SuperBlock = () => {
}
return (
<div>
<h1>{params.superblock}</h1>
<h1>{superBlockName}</h1>
<ul>
{items.map(block => (
<li key={block.name}>
<Link to={`/${params.superblock}/${block.name}`}>{block.name}</Link>
<Link to={`/${params.superblock}/${block.path}`}>{block.name}</Link>
</li>
))}
</ul>