mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: add challenge editor tool (#45214)
* feat: add challenge editor tool chore: prepare API/Client setup feat: migrate to react! feat: styling fix: useEffect loop feat: add challenge helpers feat: use actual code editor feat: styling Bring it a bit more in line with /learn * refactor: use workspaces Which unfortunately required a rollback to React 16, because having multiple React versions causes all sorts of issues. * chore: apply Oliver's review suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore: remove test files for now * fix: prettier issue * chore: apply oliver's review suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore: move scripts to root * fix: lint errors oops * chore: remove reportWebVitals thing * chore: DRY out paths * fix: create-empty-steps takes one arg * chore: start doesn't make sense now * chore: DRY out button requests * chore: one more review suggestion Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore: cleanup CRA files * fix: correct note for creating new project * feat: enable js and jsx highlighting * feat: include all superblocks * feat: improve button ux * feat: add "breadcrumbs" * feat: add link back to block from step tools * chore: remove unused deps * chore: apply oliver's review suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore: parity between file names and commands Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Generated
+14345
-21171
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@
|
|||||||
"client/plugins/fcc-source-challenges",
|
"client/plugins/fcc-source-challenges",
|
||||||
"client/plugins/gatsby-remark-node-identity",
|
"client/plugins/gatsby-remark-node-identity",
|
||||||
"curriculum",
|
"curriculum",
|
||||||
|
"tools/challenge-editor/api",
|
||||||
|
"tools/challenge-editor/client",
|
||||||
"tools/challenge-helper-scripts",
|
"tools/challenge-helper-scripts",
|
||||||
"tools/challenge-parser",
|
"tools/challenge-parser",
|
||||||
"tools/crowdin",
|
"tools/crowdin",
|
||||||
@@ -39,6 +41,9 @@
|
|||||||
"build:client": "cd ./client && npm run build",
|
"build:client": "cd ./client && npm run build",
|
||||||
"build:curriculum": "cd ./curriculum && npm run build",
|
"build:curriculum": "cd ./curriculum && npm run build",
|
||||||
"build:server": "cd ./api-server && npm run build",
|
"build:server": "cd ./api-server && npm run build",
|
||||||
|
"challenge-editor": "npm-run-all -p challenge-editor:*",
|
||||||
|
"challenge-editor:client": "cd ./tools/challenge-editor/client && npm start",
|
||||||
|
"challenge-editor:server": "cd ./tools/challenge-editor/api && npm start",
|
||||||
"clean": "npm-run-all -p clean:*",
|
"clean": "npm-run-all -p clean:*",
|
||||||
"clean-and-develop": "npm run clean && npm ci && npm run develop",
|
"clean-and-develop": "npm run clean && npm ci && npm run develop",
|
||||||
"clean:client": "cd ./client && npm run clean",
|
"clean:client": "cd ./client && npm run clean",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const CHALLENGE_DIR = join(
|
||||||
|
process.cwd(),
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'curriculum',
|
||||||
|
'challenges',
|
||||||
|
'english'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const META_DIR = join(
|
||||||
|
process.cwd(),
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'curriculum',
|
||||||
|
'challenges',
|
||||||
|
'_meta'
|
||||||
|
);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
export const superBlockList = [
|
||||||
|
{
|
||||||
|
name: 'Responsive Web Design',
|
||||||
|
path: '01-responsive-web-design'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'JavaScript Algorithms and Data Structures',
|
||||||
|
path: '02-javascript-algorithms-and-data-structures'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Front End Development Libraries',
|
||||||
|
path: '03-front-end-development-libraries'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Data Visualization',
|
||||||
|
path: '04-data-visualization'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Back End Development and APIs',
|
||||||
|
path: '05-back-end-development-and-apis'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Quality Assurance',
|
||||||
|
path: '06-quality-assurance'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Scientific Computing with Python',
|
||||||
|
path: '07-scientific-computing-with-python'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Data Analysis with Python',
|
||||||
|
path: '08-data-analysis-with-python'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Information Security',
|
||||||
|
path: '09-information-security'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Coding Interview Prep',
|
||||||
|
path: '10-coding-interview-prep'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Machine Learning with Python',
|
||||||
|
path: '11-machine-learning-with-python'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Relational Databases',
|
||||||
|
path: '13-relational-databases'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Responsive Web Design (Beta)',
|
||||||
|
path: '14-responsive-web-design-22'
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ChallengeData {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface PartialMeta {
|
||||||
|
name: string;
|
||||||
|
dashedName: string;
|
||||||
|
challengeOrder: [string, string][];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@freecodecamp/challenge-editor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Editor to help with new challenge structure",
|
||||||
|
"scripts": {
|
||||||
|
"start": "ts-node server.ts"
|
||||||
|
},
|
||||||
|
"author": "freeCodeCamp",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.3",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"ts-node": "^10.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"typescript": "^4.5.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getSteps } from '../utils/getSteps';
|
||||||
|
|
||||||
|
export const blockRoute = async (req: Request, res: Response) => {
|
||||||
|
const { superblock, block } = req.params;
|
||||||
|
|
||||||
|
const steps = await getSteps(superblock, block);
|
||||||
|
|
||||||
|
res.json(steps);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { superBlockList } from '../configs/superBlockList';
|
||||||
|
|
||||||
|
export const indexRoute = (req: Request, res: Response) => {
|
||||||
|
res.json(superBlockList);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { saveStep } from '../utils/saveStep';
|
||||||
|
|
||||||
|
export const saveRoute = async (req: Request, res: Response) => {
|
||||||
|
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 });
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getStepContent } from '../utils/getStepContent';
|
||||||
|
|
||||||
|
export const stepRoute = async (req: Request, res: Response) => {
|
||||||
|
const { superblock, block, step } = req.params;
|
||||||
|
|
||||||
|
const stepContents = await getStepContent(superblock, block, step);
|
||||||
|
res.json(stepContents);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getBlocks } from '../utils/getBlocks';
|
||||||
|
|
||||||
|
export const superblockRoute = async (req: Request, res: Response) => {
|
||||||
|
const sup = req.params.superblock;
|
||||||
|
|
||||||
|
const blocks = await getBlocks(sup);
|
||||||
|
|
||||||
|
res.json(blocks);
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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} && npm run create-next-step`);
|
||||||
|
},
|
||||||
|
'create-empty-steps': (directory, num) => {
|
||||||
|
return asyncExec(`cd ${directory} && npm run create-empty-steps ${num}`);
|
||||||
|
},
|
||||||
|
'insert-step': (directory, num) => {
|
||||||
|
return asyncExec(`cd ${directory} && npm run insert-step ${num}`);
|
||||||
|
},
|
||||||
|
'delete-step': (directory, num) => {
|
||||||
|
return asyncExec(`cd ${directory} && npm run delete-step ${num}`);
|
||||||
|
},
|
||||||
|
'update-step-titles': directory => {
|
||||||
|
return asyncExec(`cd ${directory} && npm run update-step-titles`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toolsRoute = async (req: Request, res: Response) => {
|
||||||
|
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 });
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import cors from 'cors';
|
||||||
|
import express from 'express';
|
||||||
|
import { blockRoute } from './routes/blockRoute';
|
||||||
|
import { indexRoute } from './routes/indexRoute';
|
||||||
|
import { saveRoute } from './routes/saveRoute';
|
||||||
|
import { stepRoute } from './routes/stepRoute';
|
||||||
|
import { superblockRoute } from './routes/superblockRoute';
|
||||||
|
import { toolsRoute } from './routes/toolsRoute';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: 'http://localhost:3300'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(express.static('public'));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded());
|
||||||
|
|
||||||
|
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/: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!'));
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { readdir, readFile } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { CHALLENGE_DIR, META_DIR } from '../configs/paths';
|
||||||
|
|
||||||
|
import { PartialMeta } from '../interfaces/PartialMeta';
|
||||||
|
|
||||||
|
export const getBlocks = async (sup: string) => {
|
||||||
|
const filePath = join(CHALLENGE_DIR, sup);
|
||||||
|
|
||||||
|
const files = await readdir(filePath);
|
||||||
|
const blocks = await Promise.all(
|
||||||
|
files.map(async file => {
|
||||||
|
const metaPath = join(META_DIR, file, 'meta.json');
|
||||||
|
|
||||||
|
const metaData = JSON.parse(
|
||||||
|
await readFile(metaPath, 'utf8')
|
||||||
|
) as PartialMeta;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: metaData.name,
|
||||||
|
path: file
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
) => {
|
||||||
|
const filePath = join(CHALLENGE_DIR, sup, block, step);
|
||||||
|
|
||||||
|
const fileData = await readFile(filePath, 'utf8');
|
||||||
|
const name = matter(fileData).data.title as string;
|
||||||
|
|
||||||
|
return { name, fileData };
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { readdir, readFile } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
|
import { PartialMeta } from '../interfaces/PartialMeta';
|
||||||
|
import { CHALLENGE_DIR, META_DIR } from '../configs/paths';
|
||||||
|
|
||||||
|
const getFileOrder = (id: string, meta: PartialMeta) => {
|
||||||
|
return meta.challengeOrder.findIndex(([f]) => f === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSteps = async (sup: string, block: string) => {
|
||||||
|
const filePath = join(CHALLENGE_DIR, sup, block);
|
||||||
|
|
||||||
|
const metaPath = join(META_DIR, block, 'meta.json');
|
||||||
|
|
||||||
|
const metaData = JSON.parse(await readFile(metaPath, 'utf8')) as PartialMeta;
|
||||||
|
|
||||||
|
const stepFilenames = await readdir(filePath);
|
||||||
|
const stepData = await Promise.all(
|
||||||
|
stepFilenames.map(async filename => {
|
||||||
|
const stepPath = join(filePath, 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
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return stepData.sort(
|
||||||
|
(a, b) => getFileOrder(a.id, metaData) - getFileOrder(b.id, metaData)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const filePath = join(CHALLENGE_DIR, sup, block, step);
|
||||||
|
|
||||||
|
await writeFile(filePath, content);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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*
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Block {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ChallengeContent {
|
||||||
|
name: string;
|
||||||
|
fileData: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ChallengeData {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export interface ChallengeContentRequiredProps {
|
||||||
|
superblock: string;
|
||||||
|
block: string;
|
||||||
|
challenge: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChallengeRequiredProps {
|
||||||
|
superblock: string;
|
||||||
|
block: string;
|
||||||
|
challenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockRequiredProps {
|
||||||
|
superblock: string;
|
||||||
|
block: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface SuperBlock {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.2",
|
||||||
|
"@testing-library/react": "^12.1.3",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"react": "16.14.0",
|
||||||
|
"react-codemirror2": "^7.2.1",
|
||||||
|
"react-dom": "16.14.0",
|
||||||
|
"react-router-dom": "^6.2.1",
|
||||||
|
"react-scripts": "5.0.0",
|
||||||
|
"typescript": "^4.5.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "PORT=3300 react-scripts start",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/codemirror": "^5.60.5",
|
||||||
|
"eslint-plugin-react-hooks": "^4.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import './App.css';
|
||||||
|
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/SuperBlock';
|
||||||
|
import Block from './components/block/Block';
|
||||||
|
import Editor from './components/editor/Editor';
|
||||||
|
import Tools from './components/tools/Tools';
|
||||||
|
|
||||||
|
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/:block/_tools' element={<Tools />} />
|
||||||
|
<Route path=':superblock/:block/:challenge' element={<Editor />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.step-grid {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { ChallengeData } from '../../../interfaces/ChallengeData';
|
||||||
|
import './Block.css';
|
||||||
|
|
||||||
|
const Block = () => {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [items, setItems] = useState([] as ChallengeData[]);
|
||||||
|
const params = useParams() as { superblock: string; block: string };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`http://localhost:3200/${params.superblock}/${params.block}`)
|
||||||
|
.then(res => res.json() as Promise<ChallengeData[]>)
|
||||||
|
.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>{params.block}</h1>
|
||||||
|
<span className='breadcrumb'>{params.superblock}</span>
|
||||||
|
<ul className='step-grid'>
|
||||||
|
{items.map(challenge => (
|
||||||
|
<li key={challenge.name}>
|
||||||
|
<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>
|
||||||
|
<p>
|
||||||
|
Looking to add, remove, or edit steps?{' '}
|
||||||
|
<Link to={`/${params.superblock}/${params.block}/_tools`}>
|
||||||
|
Use the step tools.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Block;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BlockRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const CreateEmptySteps = ({ superblock, block }: BlockRequiredProps) => {
|
||||||
|
const [num, setNum] = useState(0);
|
||||||
|
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(
|
||||||
|
`http://localhost:3200/${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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BlockRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const CreateNextStep = ({ superblock, block }: BlockRequiredProps) => {
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(
|
||||||
|
`http://localhost:3200/${superblock}/${block}/_tools/create-next-step`,
|
||||||
|
{
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return <button onClick={click}>Create Next Step</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateNextStep;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BlockRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const DeleteStep = ({ superblock, block }: BlockRequiredProps) => {
|
||||||
|
const [num, setNum] = useState(0);
|
||||||
|
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(`http://localhost:3200/${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;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BlockRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const InsertStep = ({ superblock, block }: BlockRequiredProps) => {
|
||||||
|
const [num, setNum] = useState(0);
|
||||||
|
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(`http://localhost:3200/${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 to insert AFTER:
|
||||||
|
<input id='num' type='number' onChange={changeNum} />
|
||||||
|
</label>
|
||||||
|
<button onClick={click}>Insert Step</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InsertStep;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ChallengeContentRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const SaveChallenge = ({
|
||||||
|
superblock,
|
||||||
|
block,
|
||||||
|
challenge,
|
||||||
|
content
|
||||||
|
}: ChallengeContentRequiredProps) => {
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(`http://localhost:3200/${superblock}/${block}/${challenge}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return <button onClick={click}>Save Changes</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaveChallenge;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BlockRequiredProps } from '../../../interfaces/PropTypes';
|
||||||
|
import { handleRequest } from '../../utils/handleRequest';
|
||||||
|
|
||||||
|
const UpdateStepTitles = ({ superblock, block }: BlockRequiredProps) => {
|
||||||
|
const click = handleRequest(() =>
|
||||||
|
fetch(
|
||||||
|
`http://localhost:3200/${superblock}/${block}/_tools/update-step-titles`,
|
||||||
|
{
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return <button onClick={click}>Reorder Steps</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateStepTitles;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
textarea {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-codemirror2 {
|
||||||
|
max-width: 80vw;
|
||||||
|
margin: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
|
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/ChallengeContent';
|
||||||
|
import SaveChallenge from '../buttons/SaveChallenge';
|
||||||
|
import './Editor.css';
|
||||||
|
|
||||||
|
const Editor = () => {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [items, setItems] = useState({
|
||||||
|
name: '',
|
||||||
|
fileData: ''
|
||||||
|
});
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const params = useParams() as {
|
||||||
|
superblock: string;
|
||||||
|
block: string;
|
||||||
|
challenge: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(
|
||||||
|
`http://localhost:3200/${params.superblock}/${params.block}/${params.challenge}`
|
||||||
|
)
|
||||||
|
.then(res => res.json() as Promise<ChallengeContent>)
|
||||||
|
.then(
|
||||||
|
content => {
|
||||||
|
setLoading(false);
|
||||||
|
setItems(content);
|
||||||
|
setInput(content.fileData);
|
||||||
|
},
|
||||||
|
(error: Error) => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
editor: codemirror.Editor,
|
||||||
|
data: codemirror.EditorChange,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
setInput(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {error.message}</div>;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{items.name}</h1>
|
||||||
|
<span className='breadcrumb'>
|
||||||
|
{params.superblock} / {params.block}
|
||||||
|
</span>
|
||||||
|
<CodeMirror
|
||||||
|
value={input}
|
||||||
|
onBeforeChange={handleChange}
|
||||||
|
options={{
|
||||||
|
mode: {
|
||||||
|
name: 'markdown',
|
||||||
|
highlightFormatting: true
|
||||||
|
},
|
||||||
|
theme: 'material',
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SaveChallenge
|
||||||
|
superblock={params.superblock}
|
||||||
|
block={params.block}
|
||||||
|
challenge={params.challenge}
|
||||||
|
content={input}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<Link to={`/${params.superblock}/${params.block}`}>
|
||||||
|
Return to Block
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Editor;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { SuperBlock } from '../../../interfaces/SuperBlock';
|
||||||
|
|
||||||
|
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('http://localhost:3200/')
|
||||||
|
.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;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { Block } from '../../../interfaces/Block';
|
||||||
|
|
||||||
|
const SuperBlock = () => {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [items, setItems] = useState([] as Block[]);
|
||||||
|
const params = useParams() as { superblock: string; block: string };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`http://localhost:3200/${params.superblock}`)
|
||||||
|
.then(res => res.json() as Promise<Block[]>)
|
||||||
|
.then(
|
||||||
|
blocks => {
|
||||||
|
setLoading(false);
|
||||||
|
setItems(blocks);
|
||||||
|
},
|
||||||
|
(error: Error) => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {error.message}</div>;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{params.superblock}</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, point to the{' '}
|
||||||
|
<code>tools/challenge-helper-scripts</code> directory, and run{' '}
|
||||||
|
<code>npm run create-project</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuperBlock;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { Link, useParams } from 'react-router-dom';
|
||||||
|
import CreateEmptySteps from '../buttons/CreateEmptySteps';
|
||||||
|
import CreateNextStep from '../buttons/CreateNextStep';
|
||||||
|
import DeleteStep from '../buttons/DeleteStep';
|
||||||
|
import InsertStep from '../buttons/InsertStep';
|
||||||
|
import UpdateStepTitles from '../buttons/UpdateStepTitles';
|
||||||
|
|
||||||
|
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 after 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've manually changed the
|
||||||
|
file order.
|
||||||
|
</p>
|
||||||
|
<UpdateStepTitles {...{ superblock, block }} />
|
||||||
|
<hr />
|
||||||
|
<Link to={`/${superblock}/${block}`}>Return to Block</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tools;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
:root {
|
||||||
|
--nav-background: #0a0a23;
|
||||||
|
--background: #1b1b32;
|
||||||
|
--content: #f5f6f7;
|
||||||
|
--grey: #3b3b4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
src: url('../public/Lato-Regular.woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Lato';
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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')
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const handleRequest = (makeRequest: () => Promise<Response>) => () => {
|
||||||
|
makeRequest()
|
||||||
|
.then(res => res.json() as Promise<{ stdout: string; stderr: string }>)
|
||||||
|
.then(data => alert(JSON.stringify(data)))
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"client/src/**/*",
|
"client/src/**/*",
|
||||||
"client/utils/**/*",
|
"client/utils/**/*",
|
||||||
"curriculum/*.test.ts",
|
"curriculum/*.test.ts",
|
||||||
|
"tools/challenge-editor/**/*",
|
||||||
"tools/challenge-helper-scripts/**/*.ts",
|
"tools/challenge-helper-scripts/**/*.ts",
|
||||||
"tools/scripts/**/*.ts"
|
"tools/scripts/**/*.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user