mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +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
|
path = curriculum/i18n-curriculum
|
||||||
url = https://github.com/freeCodeCamp/i18n-curriculum.git
|
url = https://github.com/freeCodeCamp/i18n-curriculum.git
|
||||||
ignore = dirty
|
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/is-audited.js
|
||||||
shared/utils/validate.js
|
shared/utils/validate.js
|
||||||
shared/utils/validate.test.js
|
shared/utils/validate.test.js
|
||||||
|
tools/challenge-editor
|
||||||
dist
|
dist
|
||||||
|
|||||||
+2
-3
@@ -26,9 +26,8 @@
|
|||||||
"build:client": "cd ./client && pnpm run build",
|
"build:client": "cd ./client && pnpm run build",
|
||||||
"build:curriculum": "pnpm -F=curriculum run build && pnpm -F=client run build:external-curriculum",
|
"build:curriculum": "pnpm -F=curriculum run build && pnpm -F=client run build:external-curriculum",
|
||||||
"build:api": "cd ./api && pnpm run build",
|
"build:api": "cd ./api && pnpm run build",
|
||||||
"challenge-editor": "npm-run-all -p challenge-editor:*",
|
"challenge-editor": "cd tools/challenge-editor && pnpm dev",
|
||||||
"challenge-editor:client": "cd ./tools/challenge-editor/client && pnpm start",
|
"challenge-editor-setup": "git submodule update --init tools/challenge-editor && cd tools/challenge-editor && pnpm install",
|
||||||
"challenge-editor:server": "cd ./tools/challenge-editor/api && pnpm start",
|
|
||||||
"clean": "npm-run-all -p clean:client clean:api clean:curriculum --serial clean:packages",
|
"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-and-develop": "pnpm run clean && pnpm install && pnpm run develop",
|
||||||
"clean:api": "cd api && pnpm clean",
|
"clean:api": "cd api && pnpm clean",
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ packages:
|
|||||||
- 'curriculum'
|
- 'curriculum'
|
||||||
- 'e2e'
|
- 'e2e'
|
||||||
- 'shared'
|
- 'shared'
|
||||||
- 'tools/challenge-editor/api'
|
|
||||||
- 'tools/challenge-editor/client'
|
|
||||||
- 'tools/challenge-helper-scripts'
|
- 'tools/challenge-helper-scripts'
|
||||||
- 'tools/challenge-parser'
|
- 'tools/challenge-parser'
|
||||||
- 'tools/client-plugins/*'
|
- '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