mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 10:22:16 +00:00
refactor(tools): replace challenge-editor with submodule (#65459)
This commit is contained in:
committed by
GitHub
parent
7efcd891f2
commit
79087ca9fd
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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/*'
|
||||
|
||||
Submodule
+1
Submodule tools/challenge-editor added at 2c28b22fe0
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
CHALLENGE_EDITOR_CLIENT_LOCATION=http://localhost:3300
|
||||
@@ -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!'));
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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' } } }
|
||||
];
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
CHALLENGE_EDITOR_API_LOCATION=http://localhost:3200
|
||||
CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION=http://localhost:8000
|
||||
@@ -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've manually changed the
|
||||
file order.
|
||||
</p>
|
||||
<UpdateStepTitles {...{ superblock, block }} />
|
||||
<hr />
|
||||
<Link to={`/${superblock}/${block}`}>Return to Block</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tools;
|
||||
Binary file not shown.
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user