mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-27 02:01:02 -04:00
chore(tools): display chapters in challenge editor (#62050)
This commit is contained in:
@@ -30,3 +30,14 @@ export const CHALLENGE_DIR = join(
|
||||
'english',
|
||||
'blocks'
|
||||
);
|
||||
|
||||
export const ENGLISH_LANG_DIR = join(
|
||||
process.cwd(),
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client',
|
||||
'i18n',
|
||||
'locales',
|
||||
'english'
|
||||
);
|
||||
|
||||
11
tools/challenge-editor/api/interfaces/intro.ts
Normal file
11
tools/challenge-editor/api/interfaces/intro.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
interface SuperBlock {
|
||||
title: string;
|
||||
intro: string[];
|
||||
blocks: string[];
|
||||
modules?: string[];
|
||||
chapters?: string[];
|
||||
}
|
||||
|
||||
export interface Intro {
|
||||
[key: string]: SuperBlock;
|
||||
}
|
||||
13
tools/challenge-editor/api/routes/module-block-route.ts
Normal file
13
tools/challenge-editor/api/routes/module-block-route.ts
Normal 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);
|
||||
};
|
||||
13
tools/challenge-editor/api/routes/module-route.ts
Normal file
13
tools/challenge-editor/api/routes/module-route.ts
Normal 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);
|
||||
};
|
||||
9
tools/challenge-editor/api/routes/module-step-route.ts
Normal file
9
tools/challenge-editor/api/routes/module-step-route.ts
Normal 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);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,9 @@ export interface ChallengeData {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ChallengeDataWithBlock {
|
||||
steps: ChallengeData[];
|
||||
currentBlock: string;
|
||||
currentSuperBlock: string;
|
||||
}
|
||||
|
||||
10
tools/challenge-editor/client/interfaces/chapter.ts
Normal file
10
tools/challenge-editor/client/interfaces/chapter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Module {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ChaptersWithLocation {
|
||||
modules: Module[];
|
||||
currentSuperBlock: string;
|
||||
currentChapter: string;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.step-grid {
|
||||
column-count: 3;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
.step-grid {
|
||||
column-count: 3;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
.step-grid {
|
||||
column-count: 3;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user