diff --git a/tools/challenge-editor/api/configs/paths.ts b/tools/challenge-editor/api/configs/paths.ts index b81fc8beaf2..fc185059ffe 100644 --- a/tools/challenge-editor/api/configs/paths.ts +++ b/tools/challenge-editor/api/configs/paths.ts @@ -30,3 +30,14 @@ export const CHALLENGE_DIR = join( 'english', 'blocks' ); + +export const ENGLISH_LANG_DIR = join( + process.cwd(), + '..', + '..', + '..', + 'client', + 'i18n', + 'locales', + 'english' +); diff --git a/tools/challenge-editor/api/interfaces/intro.ts b/tools/challenge-editor/api/interfaces/intro.ts new file mode 100644 index 00000000000..fca6969a8d8 --- /dev/null +++ b/tools/challenge-editor/api/interfaces/intro.ts @@ -0,0 +1,11 @@ +interface SuperBlock { + title: string; + intro: string[]; + blocks: string[]; + modules?: string[]; + chapters?: string[]; +} + +export interface Intro { + [key: string]: SuperBlock; +} diff --git a/tools/challenge-editor/api/routes/module-block-route.ts b/tools/challenge-editor/api/routes/module-block-route.ts new file mode 100644 index 00000000000..61cb2ac9896 --- /dev/null +++ b/tools/challenge-editor/api/routes/module-block-route.ts @@ -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 => { + const { superblock, chapter, module } = req.params; + + const steps = await getBlocks(superblock, chapter, module); + + res.json(steps); +}; diff --git a/tools/challenge-editor/api/routes/module-route.ts b/tools/challenge-editor/api/routes/module-route.ts new file mode 100644 index 00000000000..bf393caaf58 --- /dev/null +++ b/tools/challenge-editor/api/routes/module-route.ts @@ -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 => { + const { superblock, chapter } = req.params; + + const steps = await getModules(superblock, chapter); + + res.json(steps); +}; diff --git a/tools/challenge-editor/api/routes/module-step-route.ts b/tools/challenge-editor/api/routes/module-step-route.ts new file mode 100644 index 00000000000..e04d2b60280 --- /dev/null +++ b/tools/challenge-editor/api/routes/module-step-route.ts @@ -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 => { + const { superblock, block, step } = req.params; + + const stepContents = await getStepContent(superblock, block, step); + res.json(stepContents); +}; diff --git a/tools/challenge-editor/api/server.ts b/tools/challenge-editor/api/server.ts index e2c24cce4a9..1a201ac049d 100644 --- a/tools/challenge-editor/api/server.ts +++ b/tools/challenge-editor/api/server.ts @@ -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); }); diff --git a/tools/challenge-editor/api/utils/get-blocks.ts b/tools/challenge-editor/api/utils/get-blocks.ts index c18a11f8ec9..3671d929005 100644 --- a/tools/challenge-editor/api/utils/get-blocks.ts +++ b/tools/challenge-editor/api/utils/get-blocks.ts @@ -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 => { +type BlockLocation = { + blocks: Block[]; + currentSuperBlock: string; +}; + +const chapterBasedSuperBlocks = ['full-stack-developer']; + +export const getBlocks = async (sup: string): Promise => { 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 }; }; diff --git a/tools/challenge-editor/api/utils/get-full-stack-blocks.ts b/tools/challenge-editor/api/utils/get-full-stack-blocks.ts index 135f5b31f39..f3d82927cfb 100644 --- a/tools/challenge-editor/api/utils/get-full-stack-blocks.ts +++ b/tools/challenge-editor/api/utils/get-full-stack-blocks.ts @@ -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 => { - 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 => { + 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 => { - const superBlockDataPath = join( - SUPERBLOCK_META_DIR, - 'full-stack-developer' + '.json' - ); +export const getBlocks = async ( + superBlock: string, + chapterName: string, + moduleName: string +): Promise => { + 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 + }; }; diff --git a/tools/challenge-editor/api/utils/get-steps.ts b/tools/challenge-editor/api/utils/get-steps.ts index 99facc0b3ea..e81e3be3bad 100644 --- a/tools/challenge-editor/api/utils/get-steps.ts +++ b/tools/challenge-editor/api/utils/get-steps.ts @@ -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 => { +type StepLocation = { + steps: Step[]; + currentBlock: string; + currentSuperBlock: string; +}; + +export const getSteps = async ( + sup: string, + block: string +): Promise => { //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 => { 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 => { }) ); - 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 + }; }; diff --git a/tools/challenge-editor/client/interfaces/block.ts b/tools/challenge-editor/client/interfaces/block.ts index 5171c933506..563e6a33779 100644 --- a/tools/challenge-editor/client/interfaces/block.ts +++ b/tools/challenge-editor/client/interfaces/block.ts @@ -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; +} diff --git a/tools/challenge-editor/client/interfaces/challenge-data.ts b/tools/challenge-editor/client/interfaces/challenge-data.ts index 911a8bfea94..b1248bb2c7b 100644 --- a/tools/challenge-editor/client/interfaces/challenge-data.ts +++ b/tools/challenge-editor/client/interfaces/challenge-data.ts @@ -3,3 +3,9 @@ export interface ChallengeData { id: string; path: string; } + +export interface ChallengeDataWithBlock { + steps: ChallengeData[]; + currentBlock: string; + currentSuperBlock: string; +} diff --git a/tools/challenge-editor/client/interfaces/chapter.ts b/tools/challenge-editor/client/interfaces/chapter.ts new file mode 100644 index 00000000000..ff2473572bb --- /dev/null +++ b/tools/challenge-editor/client/interfaces/chapter.ts @@ -0,0 +1,10 @@ +export interface Module { + name: string; + path: string; +} + +export interface ChaptersWithLocation { + modules: Module[]; + currentSuperBlock: string; + currentChapter: string; +} diff --git a/tools/challenge-editor/client/src/app.tsx b/tools/challenge-editor/client/src/app.tsx index 4a132dc1ed2..7d014b84680 100644 --- a/tools/challenge-editor/client/src/app.tsx +++ b/tools/challenge-editor/client/src/app.tsx @@ -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 = () => { } /> } /> } /> + } + /> + } + /> } /> } /> diff --git a/tools/challenge-editor/client/src/components/block/block.tsx b/tools/challenge-editor/client/src/components/block/block.tsx index 0e4e24661e0..30acf1aa542 100644 --- a/tools/challenge-editor/client/src/components/block/block.tsx +++ b/tools/challenge-editor/client/src/components/block/block.tsx @@ -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(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) + .then(res => res.json() as Promise) .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 (
-

{params.block}

- {params.superblock} +

{blockName}

+ {superBlockName}
    {items.map((challenge, i) => (
  • diff --git a/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.css b/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.css new file mode 100644 index 00000000000..c2229b848a5 --- /dev/null +++ b/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.css @@ -0,0 +1,3 @@ +.step-grid { + column-count: 3; +} diff --git a/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.tsx b/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.tsx new file mode 100644 index 00000000000..3715104f152 --- /dev/null +++ b/tools/challenge-editor/client/src/components/chapter-based-block/chapter-block.tsx @@ -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(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) + .then( + superblocks => { + setLoading(false); + setItems(superblocks.steps); + setBlockName(superblocks.currentBlock); + setSuperBlockName(superblocks.currentSuperBlock); + }, + (error: Error) => { + setLoading(false); + setError(error); + } + ); + }; + + if (error) { + return
    Error: {error.message}
    ; + } + if (loading) { + return
    Loading...
    ; + } + + const isStepBasedSuperblock = stepBasedSuperblocks.includes( + params.superblock + ); + + const isTaskBasedSuperblock = taskBasedSuperblocks.includes( + params.superblock + ); + + return ( +
    +

    {blockName}

    + {superBlockName} +
      + {items.map((challenge, i) => ( +
    • + {!isStepBasedSuperblock && {`${i + 1}: `}} + + {challenge.name} + +
    • + ))} +
    +

    + Return to Blocks +

    +
    +

    Project Controls

    + {isStepBasedSuperblock ? ( +

    + Looking to add, remove, or edit steps?{' '} + + Use the step tools. + +

    + ) : isTaskBasedSuperblock ? ( + <> +

    + Looking to add or remove challenges? Navigate to
    + + freeCodeCamp/curriculum/challenges/english + {`/${params.block}/`} + +
    + in your terminal and run the following commands: +

    +
      +
    • + pnpm create-next-task: Create the next task style + challenge in this block +
    • +
    • + pnpm create-next-challenge: Create the next challenge + of a different style in this block +
    • +
    • + pnpm insert-task: Create a new task style challenge + in the middle of this block. +
    • +
    • + pnpm delete-task: Delete a task style challenge in + this block. +
    • +
    • + pnpm reorder-tasks: Rename the tasks to the correct + order. +
    • +
    +

    + Refresh the page after running a command to see the changes + reflected. +

    + + ) : ( + <> +

    + Looking to add or remove challenges? Navigate to
    + + freeCodeCamp/curriculum/challenges/english + {`/${params.superblock}/${params.block}/`} + +
    + in your terminal and run the following commands: +

    +
      +
    • + pnpm create-next-challenge: Create a new challenge at + the end of this block. +
    • +
    • + pnpm insert-challenge: Create a new challenge in the + middle of this block. +
    • +
    • + pnpm delete-challenge: Delete a challenge in this + block. +
    • +
    +

    + Refresh the page after running a command to see the changes + reflected. +

    + + )} +
    + ); +}; + +export default ChapterBasedBlock; diff --git a/tools/challenge-editor/client/src/components/chapter/chapter.css b/tools/challenge-editor/client/src/components/chapter/chapter.css new file mode 100644 index 00000000000..c2229b848a5 --- /dev/null +++ b/tools/challenge-editor/client/src/components/chapter/chapter.css @@ -0,0 +1,3 @@ +.step-grid { + column-count: 3; +} diff --git a/tools/challenge-editor/client/src/components/chapter/chapter.tsx b/tools/challenge-editor/client/src/components/chapter/chapter.tsx new file mode 100644 index 00000000000..b982394cb86 --- /dev/null +++ b/tools/challenge-editor/client/src/components/chapter/chapter.tsx @@ -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(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) + .then( + blockData => { + setLoading(false); + setItems(blockData.modules); + setChapterName(blockData.currentChapter); + setSuperBlockName(blockData.currentSuperBlock); + }, + (error: Error) => { + setLoading(false); + setError(error); + } + ); + }; + + if (error) { + return
    Error: {error.message}
    ; + } + if (loading) { + return
    Loading...
    ; + } + return ( +
    +

    {chapterName}

    +
      + {items.map(chapter => ( +
    • + + {chapter.name} + +
    • + ))} +
    +

    + Return to {superBlockName} +

    +
    +

    Create New Project

    +

    + Want to create a new project? Open your terminal and run{' '} + pnpm run create-new-project +

    +
    + ); +}; + +export default ChapterLanding; diff --git a/tools/challenge-editor/client/src/components/module/module.css b/tools/challenge-editor/client/src/components/module/module.css new file mode 100644 index 00000000000..c2229b848a5 --- /dev/null +++ b/tools/challenge-editor/client/src/components/module/module.css @@ -0,0 +1,3 @@ +.step-grid { + column-count: 3; +} diff --git a/tools/challenge-editor/client/src/components/module/module.tsx b/tools/challenge-editor/client/src/components/module/module.tsx new file mode 100644 index 00000000000..27cb6f58f16 --- /dev/null +++ b/tools/challenge-editor/client/src/components/module/module.tsx @@ -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(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) + .then( + moduleData => { + setLoading(false); + setItems(moduleData.blocks); + setModuleName(moduleData.currentModule); + setChapterName(moduleData.currentChapter); + }, + (error: Error) => { + setLoading(false); + setError(error); + } + ); + }; + + if (error) { + return
    Error: {error.message}
    ; + } + if (loading) { + return
    Loading...
    ; + } + return ( +
    +

    {moduleName}

    +
      + {items.map(block => ( +
    • + {block.name} +
    • + ))} +
    +

    + + Return to {chapterName} + +

    +
    +

    Create New Project

    +

    + Want to create a new project? Open your terminal and run{' '} + pnpm run create-new-project +

    +
    + ); +}; + +export default ModuleLanding; 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 676cec33e7e..ee7869e83b1 100644 --- a/tools/challenge-editor/client/src/components/superblock/super-block.tsx +++ b/tools/challenge-editor/client/src/components/superblock/super-block.tsx @@ -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(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) + .then(res => res.json() as Promise) .then( - blocks => { + blockData => { setLoading(false); - setItems(blocks); + setItems(blockData.blocks); + setSuperBlockName(blockData.currentSuperBlock); }, (error: Error) => { setLoading(false); @@ -38,11 +40,11 @@ const SuperBlock = () => { } return (
    -

    {params.superblock}

    +

    {superBlockName}

      {items.map(block => (
    • - {block.name} + {block.name}
    • ))}