refactor(tools): replace challenge-editor with submodule (#65459)

This commit is contained in:
Oliver Eyton-Williams
2026-01-26 12:27:08 +01:00
committed by GitHub
parent 7efcd891f2
commit 79087ca9fd
75 changed files with 8 additions and 2118 deletions
+4
View File
@@ -2,3 +2,7 @@
path = curriculum/i18n-curriculum
url = https://github.com/freeCodeCamp/i18n-curriculum.git
ignore = dirty
[submodule "tools/challenge-editor"]
path = tools/challenge-editor
url = https://github.com/freeCodeCamp/challenge-editor.git
ignore = dirty
+1
View File
@@ -20,4 +20,5 @@ shared/utils/get-lines.test.js
shared/utils/is-audited.js
shared/utils/validate.js
shared/utils/validate.test.js
tools/challenge-editor
dist
+2 -3
View File
@@ -26,9 +26,8 @@
"build:client": "cd ./client && pnpm run build",
"build:curriculum": "pnpm -F=curriculum run build && pnpm -F=client run build:external-curriculum",
"build:api": "cd ./api && pnpm run build",
"challenge-editor": "npm-run-all -p challenge-editor:*",
"challenge-editor:client": "cd ./tools/challenge-editor/client && pnpm start",
"challenge-editor:server": "cd ./tools/challenge-editor/api && pnpm start",
"challenge-editor": "cd tools/challenge-editor && pnpm dev",
"challenge-editor-setup": "git submodule update --init tools/challenge-editor && cd tools/challenge-editor && pnpm install",
"clean": "npm-run-all -p clean:client clean:api clean:curriculum --serial clean:packages",
"clean-and-develop": "pnpm run clean && pnpm install && pnpm run develop",
"clean:api": "cd api && pnpm clean",
-2
View File
@@ -4,8 +4,6 @@ packages:
- 'curriculum'
- 'e2e'
- 'shared'
- 'tools/challenge-editor/api'
- 'tools/challenge-editor/client'
- 'tools/challenge-helper-scripts'
- 'tools/challenge-parser'
- 'tools/client-plugins/*'
@@ -1,4 +0,0 @@
/* eslint-disable filenames-simple/naming-convention */
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
export default createLintStagedConfig(import.meta.dirname);
@@ -1,43 +0,0 @@
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(),
'..',
'..',
'..',
'curriculum',
'challenges',
'english',
'blocks'
);
export const ENGLISH_LANG_DIR = join(
process.cwd(),
'..',
'..',
'..',
'client',
'i18n',
'locales',
'english'
);
@@ -1,118 +0,0 @@
export const superBlockList = [
{
name: 'Legacy Responsive Web Design',
path: 'responsive-web-design'
},
{
name: 'Legacy JavaScript Algorithms and Data Structures',
path: 'javascript-algorithms-and-data-structures'
},
{
name: 'Front End Development Libraries',
path: 'front-end-development-libraries'
},
{
name: 'Data Visualization',
path: 'data-visualization'
},
{
name: 'Back End Development and APIs',
path: 'back-end-development-and-apis'
},
{
name: 'Quality Assurance',
path: 'quality-assurance'
},
{
name: 'Scientific Computing with Python',
path: 'scientific-computing-with-python'
},
{
name: 'Data Analysis with Python',
path: 'data-analysis-with-python'
},
{
name: 'Information Security',
path: 'information-security'
},
{
name: 'Coding Interview Prep',
path: 'coding-interview-prep'
},
{
name: 'Machine Learning with Python',
path: 'machine-learning-with-python'
},
{
name: 'Relational Databases',
path: 'relational-databases'
},
{
name: 'Responsive Web Design',
path: 'responsive-web-design-22'
},
{
name: 'JavaScript Algorithms and Data Structures',
path: 'javascript-algorithms-and-data-structures-22'
},
{
name: 'The Odin Project',
path: 'the-odin-project'
},
{
name: 'College Algebra with Python',
path: 'college-algebra-with-python'
},
{
name: 'Project Euler',
path: 'project-euler'
},
{
name: '(New) Foundational C# with Microsoft',
path: 'foundational-c-sharp-with-microsoft'
},
{
name: 'A2 English for Developers',
path: 'a2-english-for-developers'
},
{
name: 'Rosetta Code',
path: 'rosetta-code'
},
{
name: 'Python For Everybody',
path: 'python-for-everybody'
},
{
name: 'B1 English for Developers (Beta)',
path: 'b1-english-for-developers'
},
{
name: 'Certified Full Stack Developer',
path: 'full-stack-developer'
},
{
name: 'A1 Professional Spanish (Beta)',
path: 'a1-professional-spanish'
},
{
name: 'A2 Professional Spanish (Beta)',
path: 'a2-professional-spanish'
},
{
name: 'A2 Professional Chinese (Beta)',
path: 'a2-professional-chinese'
},
{
name: 'Basic HTML',
path: 'basic-html'
},
{
name: 'Semantic HTML',
path: 'semantic-html'
},
{
name: 'A1 Professional Chinese (Beta)',
path: 'a1-professional-chinese'
}
];
@@ -1,3 +0,0 @@
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
export default configTypeChecked;
@@ -1,5 +0,0 @@
export interface ChallengeData {
name: string;
id: string;
path: string;
}
@@ -1,11 +0,0 @@
interface SuperBlock {
title: string;
intro: string[];
blocks: string[];
modules?: string[];
chapters?: string[];
}
export interface Intro {
[key: string]: SuperBlock;
}
@@ -1,5 +0,0 @@
export interface PartialMeta {
name: string;
dashedName: string;
challengeOrder: { id: string; title: string }[];
}
@@ -1,14 +0,0 @@
export interface SuperBlockModule {
dashedName: string;
blocks?: string[];
}
export interface SuperBlockChapter {
dashedName: string;
modules: SuperBlockModule[];
}
export interface SuperBlockMeta {
blocks?: string[];
chapters?: SuperBlockChapter[];
}
@@ -1,16 +0,0 @@
type ToolsFunction = (
directory: string
) => Promise<{ stdout: string; stderr: string }>;
type ToolsFunctionWithArg = (
directory: string,
start: number
) => Promise<{ stdout: string; stderr: string }>;
export interface ToolsSwitch {
'create-next-step': ToolsFunction;
'create-empty-steps': ToolsFunctionWithArg;
'insert-step': ToolsFunctionWithArg;
'delete-step': ToolsFunctionWithArg;
'update-step-titles': ToolsFunction;
}
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@freecodecamp/challenge-editor-api",
"version": "1.0.0",
"private": true,
"description": "Editor to help with new challenge structure",
"scripts": {
"start": "tsx server.ts",
"postinstall": "shx cp ./sample.env ./.env",
"lint": "eslint --max-warnings 0",
"type-check": "tsc --noEmit"
},
"author": "freeCodeCamp",
"license": "BSD-3-Clause",
"dependencies": {
"cors": "2.8.5",
"express": "4.18.2",
"gray-matter": "4.0.3"
},
"devDependencies": {
"@freecodecamp/eslint-config": "workspace:*",
"@total-typescript/ts-reset": "^0.6.1",
"@types/cors": "^2.8.13",
"@types/express": "4.17.21",
"dotenv": "16.4.5",
"eslint": "^9.39.1",
"shx": "0.3.4",
"typescript": "5.9.3"
}
}
-1
View File
@@ -1 +0,0 @@
import '@total-typescript/ts-reset';
@@ -1,13 +0,0 @@
import { Request, Response } from 'express';
import { getSteps } from '../utils/get-steps';
export const blockRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, block } = req.params;
const steps = await getSteps(superblock, block);
res.json(steps);
};
@@ -1,6 +0,0 @@
import { Request, Response } from 'express';
import { superBlockList } from '../configs/super-block-list';
export const indexRoute = (req: Request, res: Response): void => {
res.json(superBlockList);
};
@@ -1,13 +0,0 @@
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);
};
@@ -1,13 +0,0 @@
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);
};
@@ -1,9 +0,0 @@
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);
};
@@ -1,15 +0,0 @@
import { Request, Response } from 'express';
import { saveStep } from '../utils/save-step';
export const saveRoute = async (req: Request, res: Response): Promise<void> => {
const { superblock, block, step } = req.params;
const content = (req.body as { content: string }).content;
const success = await saveStep(superblock, block, step, content);
const message = success
? 'Your changes have been saved and are ready to commit!'
: 'There was an error when saving your changes. Please try again.';
res.json({ message });
};
@@ -1,9 +0,0 @@
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);
};
@@ -1,13 +0,0 @@
import { Request, Response } from 'express';
import { getBlocks } from '../utils/get-blocks';
export const superblockRoute = async (
req: Request,
res: Response
): Promise<void> => {
const sup = req.params.superblock;
const blocks = await getBlocks(sup);
res.json(blocks);
};
@@ -1,56 +0,0 @@
import { exec } from 'child_process';
import { join } from 'path';
import { promisify } from 'util';
import { Request, Response } from 'express';
import { ToolsSwitch } from '../interfaces/tools';
const asyncExec = promisify(exec);
const toolsSwitch: ToolsSwitch = {
'create-next-step': directory => {
return asyncExec(`cd ${directory} && pnpm run create-next-step`);
},
'create-empty-steps': (directory, num) => {
return asyncExec(`cd ${directory} && pnpm run create-empty-steps ${num}`);
},
'insert-step': (directory, num) => {
return asyncExec(`cd ${directory} && pnpm run insert-step ${num}`);
},
'delete-step': (directory, num) => {
return asyncExec(`cd ${directory} && pnpm run delete-step ${num}`);
},
'update-step-titles': directory => {
return asyncExec(`cd ${directory} && pnpm run update-step-titles`);
}
};
export const toolsRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, block, command } = req.params;
const { num } = req.body as Record<string, number>;
const directory = join(
__dirname,
'..',
'..',
'..',
'..',
'curriculum',
'challenges',
'english',
superblock,
block
);
if (!(command in toolsSwitch)) {
res.json({ stdout: '', stderr: 'Command not found' });
return;
}
const parsed = command as keyof ToolsSwitch;
const { stdout, stderr } = await toolsSwitch[parsed](directory, num);
res.json({ stdout, stderr });
};
-1
View File
@@ -1 +0,0 @@
CHALLENGE_EDITOR_CLIENT_LOCATION=http://localhost:3300
-56
View File
@@ -1,56 +0,0 @@
import * as dotenv from 'dotenv';
dotenv.config();
import cors from 'cors';
import express from 'express';
import { blockRoute } from './routes/block-route';
import { indexRoute } from './routes/index-route';
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();
app.use(
cors({
origin: process.env.CHALLENGE_EDITOR_CLIENT_LOCATION
})
);
app.use(express.static('public'));
app.use(express.json());
app.post('/:superblock/:block/_tools/:command', (req, res, next) => {
toolsRoute(req, res).catch(next);
});
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);
});
app.get('/:superblock/:block', (req, res, next) => {
blockRoute(req, res).catch(next);
});
app.get('/:superblock', (req, res, next) => {
superblockRoute(req, res).catch(next);
});
app.get('/', indexRoute);
app.listen(3200, () => console.log('App is live on 3200!'));
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../../../tsconfig-base.json"
}
@@ -1,69 +0,0 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { chapterBasedSuperBlocks } from '../../../../packages/shared/src/config/curriculum';
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;
};
type BlockLocation = {
blocks: Block[];
currentSuperBlock: string;
};
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 (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 blockStructurePath = join(BLOCK_META_DIR, block + '.json');
const blockMetaFile = await readFile(blockStructurePath, {
encoding: 'utf8'
});
const blockMeta = JSON.parse(blockMetaFile) as PartialMeta;
return {
name: blockMeta.name,
path: block
};
})
);
}
return { blocks: blocks, currentSuperBlock: introData[sup]?.title };
};
@@ -1,131 +0,0 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
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;
};
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];
const chapterTrueName = chapters.filter(x => x[0] === chap)[0][1];
let modules: Module[] = [];
modules = 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 (
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 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 blockStructurePath = join(BLOCK_META_DIR, block + '.json');
const blockMetaFile = await readFile(blockStructurePath, {
encoding: 'utf8'
});
const blockMeta = JSON.parse(blockMetaFile) as PartialMeta;
return {
name: blockMeta.name,
path: block
};
})
);
return {
blocks: blocks,
currentModule: moduleTrueName,
currentChapter: chapterTrueName
};
};
@@ -1,17 +0,0 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import matter from 'gray-matter';
import { CHALLENGE_DIR } from '../configs/paths';
export const getStepContent = async (
sup: string,
block: string,
step: string
): Promise<{ name: string; dashedName: string; fileData: string }> => {
const filePath = join(CHALLENGE_DIR, block, step);
const fileData = await readFile(filePath, 'utf8');
const name = matter(fileData).data.title as string;
const dashedName = matter(fileData).data.dashedName as string;
return { name, dashedName, fileData };
};
@@ -1,81 +0,0 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import matter from 'gray-matter';
import { PartialMeta } from '../interfaces/partial-meta';
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);
};
type Step = {
name: string;
id: string;
path: string;
};
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(
// await readFile(superMetaPath, 'utf8')
//) as Partial;
const stepDirectory = join(CHALLENGE_DIR, block);
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;
const stepFileNames = blockMetaData.challengeOrder.map(x => x.id + '.md');
const stepData = await Promise.all(
stepFileNames.map(async filename => {
const stepPath = join(stepDirectory, filename);
const step = await readFile(stepPath, 'utf8');
const frontMatter = matter(step);
return {
name: frontMatter.data.title as string,
id: frontMatter.data.id as string,
path: filename
};
})
);
const steps = stepData.sort(
(a, b) =>
getFileOrder(a.id, blockMetaData) - getFileOrder(b.id, blockMetaData)
);
return {
steps: steps,
currentBlock: blockMetaData.name,
currentSuperBlock: introData[sup]?.title
};
};
@@ -1,21 +0,0 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';
import { CHALLENGE_DIR } from '../configs/paths';
export const saveStep = async (
sup: string,
block: string,
step: string,
content: string
): Promise<boolean> => {
try {
const filePath = join(CHALLENGE_DIR, block, step);
await writeFile(filePath, content);
return true;
} catch (err) {
console.log(err);
return false;
}
};
-23
View File
@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -1,4 +0,0 @@
/* eslint-disable filenames-simple/naming-convention */
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
export default createLintStagedConfig(import.meta.dirname);
@@ -1,10 +0,0 @@
import {
configTypeChecked,
configReact
} from '@freecodecamp/eslint-config/base';
export default [
...configTypeChecked,
...configReact,
{ settings: { react: { version: '17.0.2' } } }
];
-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>freeCodeCamp Challenge Editor</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
@@ -1,15 +0,0 @@
export interface Block {
name: string;
path: string;
}
export interface BlocksWithSuperBlock {
blocks: Block[];
currentSuperBlock: string;
}
export interface BlocksWithModule {
blocks: Block[];
currentModule: string;
currentChapter: string;
}
@@ -1,5 +0,0 @@
export interface ChallengeContent {
name: string;
fileData: string;
dashedName: string;
}
@@ -1,11 +0,0 @@
export interface ChallengeData {
name: string;
id: string;
path: string;
}
export interface ChallengeDataWithBlock {
steps: ChallengeData[];
currentBlock: string;
currentSuperBlock: string;
}
@@ -1,10 +0,0 @@
export interface Module {
name: string;
path: string;
}
export interface ChaptersWithLocation {
modules: Module[];
currentSuperBlock: string;
currentChapter: string;
}
@@ -1,9 +0,0 @@
export interface BlockRequiredProps {
superblock?: string;
block?: string;
}
export interface ChallengeContentRequiredProps extends BlockRequiredProps {
challenge?: string;
content: string;
}
@@ -1,4 +0,0 @@
export interface SuperBlock {
name: string;
path: string;
}
@@ -1,30 +0,0 @@
{
"name": "@freecodecamp/challenge-editor-client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@vitejs/plugin-react": "4.2.1",
"codemirror": "5.65.16",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "6.18.0",
"vite": "4.5.2"
},
"scripts": {
"start": "PORT=3300 vite",
"build": "tsc && vite build",
"lint": "eslint --max-warnings 0",
"type-check": "tsc --noEmit",
"postinstall": "shx cp ./sample.env ./.env"
},
"devDependencies": {
"@freecodecamp/eslint-config": "workspace:*",
"@types/codemirror": "5.60.15",
"@types/react": "17.0.83",
"@types/react-dom": "17.0.19",
"@uiw/react-codemirror": "3.2.10",
"eslint": "^9.39.1",
"shx": "0.3.4",
"typescript": "5.9.3"
}
}
-2
View File
@@ -1,2 +0,0 @@
CHALLENGE_EDITOR_API_LOCATION=http://localhost:3200
CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION=http://localhost:8000
-37
View File
@@ -1,37 +0,0 @@
import * as React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/header/header';
import Landing from './components/landing/landing';
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 (
<div className='app'>
<Header />
<Router>
<Routes>
<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>
</Router>
</div>
);
};
export default App;
@@ -1,3 +0,0 @@
.step-grid {
column-count: 3;
}
@@ -1,173 +0,0 @@
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 './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',
'a1-professional-spanish',
'a2-professional-spanish',
'a2-professional-chinese',
'a1-professional-chinese'
];
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(() => {
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>
curriculum/challenges/english/blocks
{`/${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>
curriculum/challenges/english/blocks
{`/${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 Block;
@@ -1,38 +0,0 @@
import React, { useState } from 'react';
import { BlockRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const CreateEmptySteps = ({ superblock, block }: BlockRequiredProps) => {
const [num, setNum] = useState(0);
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${
block || ''
}/_tools/create-empty-steps`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ num })
}
)
);
const changeNum = (e: React.ChangeEvent<HTMLInputElement>) => {
setNum(parseInt(e.target.value, 10));
};
return (
<div>
<label htmlFor='num'>
Number of steps to create:
<input id='num' type='number' onChange={changeNum} />
</label>
<button onClick={click}>Create Empty Steps</button>
</div>
);
};
export default CreateEmptySteps;
@@ -1,20 +0,0 @@
import React from 'react';
import { BlockRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const CreateNextStep = ({ superblock, block }: BlockRequiredProps) => {
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${
block || ''
}/_tools/create-next-step`,
{
method: 'POST'
}
)
);
return <button onClick={click}>Create Next Step</button>;
};
export default CreateNextStep;
@@ -1,36 +0,0 @@
import React, { useState } from 'react';
import { BlockRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const DeleteStep = ({ superblock, block }: BlockRequiredProps) => {
const [num, setNum] = useState(0);
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${block || ''}/_tools/delete-step`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ num })
}
)
);
const changeNum = (e: React.ChangeEvent<HTMLInputElement>) => {
setNum(parseInt(e.target.value, 10));
};
return (
<div>
<label htmlFor='num'>
Step to delete:
<input id='num' type='number' onChange={changeNum} />
</label>
<button onClick={click}>Delete Step</button>
</div>
);
};
export default DeleteStep;
@@ -1,36 +0,0 @@
import React, { useState } from 'react';
import { BlockRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const InsertStep = ({ superblock, block }: BlockRequiredProps) => {
const [num, setNum] = useState(0);
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${block || ''}/_tools/insert-step`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ num })
}
)
);
const changeNum = (e: React.ChangeEvent<HTMLInputElement>) => {
setNum(parseInt(e.target.value, 10));
};
return (
<div>
<label htmlFor='num'>
Step Number:
<input id='num' type='number' onChange={changeNum} />
</label>
<button onClick={click}>Insert Step</button>
</div>
);
};
export default InsertStep;
@@ -1,27 +0,0 @@
import React from 'react';
import { ChallengeContentRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const SaveChallenge = ({
superblock,
block,
challenge,
content
}: ChallengeContentRequiredProps) => {
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${block || ''}/${challenge || ''}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
}
)
);
return <button onClick={click}>Save Changes</button>;
};
export default SaveChallenge;
@@ -1,20 +0,0 @@
import React from 'react';
import { BlockRequiredProps } from '../../../interfaces/prop-types';
import { API_LOCATION, handleRequest } from '../../utils/handle-request';
const UpdateStepTitles = ({ superblock, block }: BlockRequiredProps) => {
const click = handleRequest(() =>
fetch(
`${API_LOCATION}/${superblock || ''}/${
block || ''
}/_tools/update-step-titles`,
{
method: 'POST'
}
)
);
return <button onClick={click}>Reorder Steps</button>;
};
export default UpdateStepTitles;
@@ -1,3 +0,0 @@
.step-grid {
column-count: 3;
}
@@ -1,177 +0,0 @@
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>
curriculum/challenges/english/blocks
{`/${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>
curriculum/challenges/english/blocks
{`/${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;
@@ -1,3 +0,0 @@
.step-grid {
column-count: 3;
}
@@ -1,70 +0,0 @@
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;
@@ -1,15 +0,0 @@
textarea {
display: block;
margin: auto;
width: 500px;
max-width: 100vw;
height: 500px;
}
.CodeMirror {
/* need to add important to overwrite the specificity of the classes in the dependency */
height: 70vh !important;
max-width: 80vw;
margin: auto;
text-align: left;
}
@@ -1,117 +0,0 @@
import React, { useEffect, useState } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import * as codemirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/markdown/markdown';
// we need to import this mode to get the fenced codeblock highlighting
import 'codemirror/mode/css/css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/jsx/jsx';
import { Link, useParams } from 'react-router-dom';
import { ChallengeContent } from '../../../interfaces/challenge-content';
import SaveChallenge from '../buttons/save-challenge';
import './editor.css';
import { API_LOCATION } from '../../utils/handle-request';
// only includes superblocks whose folder names don't match their dashed names?
export const superBlockNameMap: { [key: string]: string } = {
'responsive-web-design-22': '2022/responsive-web-design',
'javascript-algorithms-and-data-structures-22':
'javascript-algorithms-and-data-structures-v8',
'front-end-development': 'full-stack-developer'
};
const Editor = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState({
name: '',
dashedName: '',
fileData: ''
});
const [stepContent, setStepContent] = useState('');
const { superblock = '', block = '', challenge = '' } = useParams();
const superblockUrl =
superblock in superBlockNameMap
? superBlockNameMap[superblock]
: superblock;
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${superblock}/${block}/${challenge}`)
.then(res => res.json() as Promise<ChallengeContent>)
.then(
content => {
setLoading(false);
setItems(content);
setStepContent(content.fileData);
},
(error: Error) => {
setLoading(false);
setError(error);
}
);
};
const handleChange = (instance: codemirror.Editor) => {
const editedContent = instance.getValue();
setStepContent(editedContent);
};
if (error) {
return <div>Error: {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{items.name}</h1>
<span className='breadcrumb'>
{superblock} / {block}
</span>
<CodeMirror
value={stepContent}
onChange={handleChange}
options={{
mode: {
name: 'markdown',
highlightFormatting: true
},
theme: 'material',
lineNumbers: true,
lineWrapping: true
}}
/>
<SaveChallenge
superblock={superblock}
block={block}
challenge={challenge}
content={stepContent}
/>
<p>
<Link to={`/${superblock}/${block}`}>Return to Block</Link>
</p>
<p>
<Link
to={`${import.meta.env.CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION}/learn/${superblockUrl}/${block || ''}/${
items.dashedName
}`}
target='_blank'
>
View Live Version of the Challenge in your running development
environment
</Link>
</p>
</div>
);
};
export default Editor;
@@ -1,16 +0,0 @@
.header {
width: 100vw;
margin-top: 0;
text-align: center;
background: var(--background);
color: var(--content);
}
.header a {
color: var(--content);
}
.header p {
margin: 0;
font-size: 2rem;
}
@@ -1,14 +0,0 @@
import React from 'react';
import './header.css';
const Header = () => {
return (
<div className='header'>
<p>
<a href='/'>freeCodeCamp Challenge Editor</a>
</p>
</div>
);
};
export default Header;
@@ -1,51 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { SuperBlock } from '../../../interfaces/super-block';
import { API_LOCATION } from '../../utils/handle-request';
const Landing = () => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([] as SuperBlock[]);
useEffect(() => {
fetchData();
}, []);
const fetchData = () => {
setLoading(true);
fetch(API_LOCATION)
.then(res => res.json() as Promise<SuperBlock[]>)
.then(
superblocks => {
setLoading(false);
setItems(superblocks);
},
(error: Error) => {
setLoading(false);
setError(error);
}
);
};
if (error) {
return <div>Error: {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Superblocks</h1>
<ul>
{items.map(superblock => (
<li key={superblock.name}>
<Link to={`/${superblock.path}`}>{superblock.name}</Link>
</li>
))}
</ul>
</div>
);
};
export default Landing;
@@ -1,3 +0,0 @@
.step-grid {
column-count: 3;
}
@@ -1,74 +0,0 @@
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,64 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
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(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchData = () => {
setLoading(true);
fetch(`${API_LOCATION}/${params.superblock}`)
.then(res => res.json() as Promise<BlocksWithSuperBlock>)
.then(
blockData => {
setLoading(false);
setItems(blockData.blocks);
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>{superBlockName}</h1>
<ul>
{items.map(block => (
<li key={block.name}>
<Link to={`/${params.superblock}/${block.path}`}>{block.name}</Link>
</li>
))}
</ul>
<p>
<Link to={'/'}>Return to Superblocks</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 SuperBlock;
@@ -1,13 +0,0 @@
label {
display: block;
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
input {
background-color: var(--grey);
border: 2px solid var(--content);
padding: 6px 12px;
margin-left: 10px;
color: white;
}
@@ -1,51 +0,0 @@
import React, { Link, useParams } from 'react-router-dom';
import CreateEmptySteps from '../buttons/create-empty-steps';
import CreateNextStep from '../buttons/create-next-step';
import DeleteStep from '../buttons/delete-step';
import InsertStep from '../buttons/insert-step';
import UpdateStepTitles from '../buttons/update-step-titles';
import './tools.css';
const Tools = () => {
const { block, superblock } = useParams() as {
block: string;
superblock: string;
};
return (
<div>
<h1>Editing Steps for {block}</h1>
<p>These tools will allow you to create, delete, and reorder steps.</p>
<h2>Create Next Step</h2>
<p>This tool creates a new step at the end of the project.</p>
<CreateNextStep {...{ superblock, block }} />
<h2>Create Empty Steps</h2>
<p>
This tool creates <code>n</code> number of empty steps at the end of the
project.
</p>
<CreateEmptySteps {...{ superblock, block }} />
<h2>Insert Step</h2>
<p>
This tool inserts a new step as the <code>nth</code> step.
</p>
<InsertStep {...{ superblock, block }} />
<h2>Delete Step</h2>
<p>
This tool deletes step <code>n</code>.
</p>
<DeleteStep {...{ superblock, block }} />
<h2>Update Step Titles</h2>
<p>
This reorders the existing steps, updating the meta for the block. You
should not need to use this one unless you&apos;ve manually changed the
file order.
</p>
<UpdateStepTitles {...{ superblock, block }} />
<hr />
<Link to={`/${superblock}/${block}`}>Return to Block</Link>
</div>
);
};
export default Tools;
@@ -1,62 +0,0 @@
:root {
--nav-background: #0a0a23;
--background: #1b1b32;
--content: #f5f6f7;
--grey: #3b3b4f;
--font-family-sans-serif: 'Lato', sans-serif;
--font-family-monospace: 'Hack-ZeroSlash', monospace;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato-Regular.woff');
}
body {
margin: 0;
padding: 0;
font-family: var(--font-family-sans-serif);
text-align: center;
background: var(--background);
color: var(--content);
}
ul {
list-style-type: none;
padding: 0;
}
p,
li {
font-size: 1.2rem;
}
a {
color: var(--content);
}
button {
text-align: center;
vertical-align: middle;
border: 3px solid var(--content);
font-size: 16pt;
padding: 6px 12px;
margin: 10px auto;
background: var(--grey);
color: var(--content);
cursor: pointer;
}
button:active {
color: var(--background);
background: var(--content);
}
code {
background: var(--grey);
}
.breadcrumb {
font-size: 1.2rem;
font-style: italic;
}
@@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './app';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
@@ -1,22 +0,0 @@
export const handleRequest = (makeRequest: () => Promise<Response>) => () => {
makeRequest()
.then(
res =>
res.json() as Promise<{
stdout?: string;
stderr?: string;
message?: string;
}>
)
.then(data => {
if (data.message) {
alert(data.message);
} else {
alert(JSON.stringify(data));
}
})
.catch(err => console.error(err));
};
export const API_LOCATION = import.meta.env
.CHALLENGE_EDITOR_API_LOCATION as string;
-9
View File
@@ -1,9 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"]
}
}
@@ -1,12 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
base: '/',
plugins: [react()],
envPrefix: 'CHALLENGE_EDITOR_',
server: {
port: 3300
}
});