mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-24 11:03:17 -04:00
refactor(curriculum): remove block name metadata and source titles from intro (#66415)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
superBlockStages,
|
||||
SuperBlockStage
|
||||
} from '@freecodecamp/shared/config/curriculum';
|
||||
import { getCurriculum } from '../tools/get-curriculum';
|
||||
import intro from './locales/english/intro.json';
|
||||
|
||||
interface Intro {
|
||||
@@ -110,3 +111,38 @@ describe('Intro file structure tests:', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type SuperBlockInfo = {
|
||||
blocks: Record<string, unknown>;
|
||||
};
|
||||
|
||||
describe('Curriculum validation', () => {
|
||||
const curriculum = getCurriculum() as Record<string, SuperBlockInfo>;
|
||||
// certifications are not superblocks, they're just mixed in with them.
|
||||
const superblocks = Object.entries(curriculum).filter(
|
||||
([key]) => key !== 'certifications'
|
||||
);
|
||||
|
||||
// It's important that we check that each block in the curriculum has a title
|
||||
// in the intro, rather than the other way around, because the intro must
|
||||
// include upcoming changes. The curriculum only does if SHOW_UPCOMING_CHANGES
|
||||
// is true.
|
||||
superblocks.forEach(superblock => {
|
||||
const [name, superBlockInfo] = superblock;
|
||||
const blockObject = superBlockInfo.blocks;
|
||||
describe(`${name}`, () => {
|
||||
test('should have titles for each block in intro.json', () => {
|
||||
const blocks = Object.keys(blockObject);
|
||||
|
||||
blocks.forEach(block => {
|
||||
const blockFromIntro = (intro as unknown as Intro)[superblock[0]]
|
||||
.blocks[block];
|
||||
expect(
|
||||
blockFromIntro.title,
|
||||
`block ${block} needs a non-empty title`
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9904,7 +9904,7 @@
|
||||
"title": "Full-Stack Open",
|
||||
"intro": ["A good intro is to be added here."],
|
||||
"blocks": {
|
||||
"cat-blog-page": {
|
||||
"workshop-blog-page": {
|
||||
"title": "Build a Cat Blog Page",
|
||||
"intro": [
|
||||
"In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements."
|
||||
|
||||
@@ -161,21 +161,17 @@ describe('external curriculum data build', () => {
|
||||
superBlock
|
||||
] as GeneratedBlockBasedCurriculumProps;
|
||||
|
||||
// Temporary skip these checks to keep CI stable.
|
||||
// TODO: uncomment these once https://github.com/freeCodeCamp/freeCodeCamp/issues/60660 is completed.
|
||||
|
||||
// Randomly pick a block to check its data.
|
||||
// const blocks = superBlockData.blocks;
|
||||
// const randomBlockIndex = Math.floor(Math.random() * blocks.length);
|
||||
// const randomBlock = blocks[randomBlockIndex];
|
||||
|
||||
expect(superBlockData.intro).toEqual(intros[superBlock].intro);
|
||||
// expect(superBlockData.blocks[randomBlockIndex].intro).toEqual(
|
||||
// intros[superBlock].blocks[randomBlock.meta.dashedName as string].intro
|
||||
// );
|
||||
// expect(superBlockData.blocks[randomBlockIndex].meta.name).toEqual(
|
||||
// intros[superBlock].blocks[randomBlock.meta.dashedName as string].title
|
||||
// );
|
||||
const blocks = superBlockData.blocks;
|
||||
|
||||
for (const block of blocks) {
|
||||
expect(block.intro).toEqual(
|
||||
intros[superBlock].blocks[block.meta.dashedName as string].intro
|
||||
);
|
||||
expect(block.meta.name).toEqual(
|
||||
intros[superBlock].blocks[block.meta.dashedName as string].title
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -244,22 +240,22 @@ describe('external curriculum data build', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Temporary skip these checks to keep CI stable.
|
||||
// TODO: uncomment these once https://github.com/freeCodeCamp/freeCodeCamp/issues/60660 is completed.
|
||||
for (const chapter of superBlockData.chapters) {
|
||||
if (chapter.comingSoon) continue;
|
||||
|
||||
// Check block data
|
||||
// expect(
|
||||
// superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
|
||||
// .blocks[randomBlockIndex].intro
|
||||
// ).toEqual(
|
||||
// superBlockIntros.blocks[randomBlock.meta.dashedName as string].intro
|
||||
// );
|
||||
// expect(
|
||||
// superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
|
||||
// .blocks[randomBlockIndex].meta.name
|
||||
// ).toEqual(
|
||||
// superBlockIntros.blocks[randomBlock.meta.dashedName as string].title
|
||||
// );
|
||||
for (const module of chapter.modules) {
|
||||
if (module.comingSoon) continue;
|
||||
|
||||
for (const block of module.blocks) {
|
||||
expect(block.intro).toEqual(
|
||||
superBlockIntros.blocks[block.meta.dashedName as string].intro
|
||||
);
|
||||
expect(block.meta.name).toEqual(
|
||||
superBlockIntros.blocks[block.meta.dashedName as string].title
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -376,9 +376,15 @@ export function buildExtCurriculumDataV2(
|
||||
.filter(block => blocksWithData[block])
|
||||
.map(block => {
|
||||
const blockData = blocksWithData[block];
|
||||
const blockIntro = superBlockIntros.blocks[block];
|
||||
return {
|
||||
intro: superBlockIntros.blocks[block].intro,
|
||||
meta: omit(blockData.meta, ['chapter', 'module'])
|
||||
intro: blockIntro.intro,
|
||||
// Keep `meta.name` for backward compatibility with
|
||||
// consumers that have not migrated to intro-based titles.
|
||||
meta: {
|
||||
...omit(blockData.meta, ['chapter', 'module']),
|
||||
name: blockIntro.title
|
||||
}
|
||||
};
|
||||
})
|
||||
}))
|
||||
@@ -398,10 +404,13 @@ export function buildExtCurriculumDataV2(
|
||||
const blockNames = Object.keys(curriculum[superBlockKey].blocks);
|
||||
const blocks = blockNames.map(blockName => {
|
||||
const blockData = curriculum[superBlockKey].blocks[blockName];
|
||||
const blockIntro = intros[superBlockKey].blocks[blockName];
|
||||
|
||||
return {
|
||||
intro: intros[superBlockKey].blocks[blockName].intro,
|
||||
meta: blockData.meta
|
||||
intro: blockIntro.intro,
|
||||
// Keep `meta.name` for backward compatibility with
|
||||
// consumers that have not migrated to intro-based titles.
|
||||
meta: { ...blockData.meta, name: blockIntro.title }
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const CURRICULUM_PATH = '../../../curriculum/generated/curriculum.json';
|
||||
// const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
|
||||
const curriculum = JSON.parse(
|
||||
readFileSync(join(__dirname, CURRICULUM_PATH), 'utf-8')
|
||||
);
|
||||
|
||||
import { getCurriculum } from '../get-curriculum';
|
||||
import {
|
||||
buildExtCurriculumDataV2,
|
||||
Curriculum as CurriculumV2,
|
||||
@@ -24,5 +15,5 @@ if (isSelectiveBuild) {
|
||||
'Skipping external curriculum build (selective build mode active)'
|
||||
);
|
||||
} else {
|
||||
buildExtCurriculumDataV2(curriculum as CurriculumV2<CurriculumPropsV2>);
|
||||
buildExtCurriculumDataV2(getCurriculum() as CurriculumV2<CurriculumPropsV2>);
|
||||
}
|
||||
|
||||
8
client/tools/get-curriculum.ts
Normal file
8
client/tools/get-curriculum.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const CURRICULUM_PATH = '../../curriculum/generated/curriculum.json';
|
||||
|
||||
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
|
||||
export const getCurriculum = () =>
|
||||
JSON.parse(readFileSync(join(__dirname, CURRICULUM_PATH), 'utf-8'));
|
||||
69
curriculum/schema/intro-schema.js
Normal file
69
curriculum/schema/intro-schema.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const blockIntroSchema = Joi.object()
|
||||
.keys({
|
||||
title: Joi.string().trim().min(1).required(),
|
||||
intro: Joi.array().items(Joi.string().allow('')).required()
|
||||
})
|
||||
.unknown(true);
|
||||
|
||||
const superBlockIntroSchema = Joi.object()
|
||||
.keys({
|
||||
title: Joi.string().trim().min(1).required(),
|
||||
intro: Joi.array().items(Joi.string().allow('')).required(),
|
||||
blocks: Joi.object().pattern(Joi.string(), blockIntroSchema).required()
|
||||
})
|
||||
.unknown(true);
|
||||
|
||||
function createIntroSchema(expectedBlocksBySuperblock) {
|
||||
return Joi.object()
|
||||
.unknown(true)
|
||||
.custom((intros, helpers) => {
|
||||
for (const [superblock, blocks] of Object.entries(
|
||||
expectedBlocksBySuperblock
|
||||
)) {
|
||||
const superBlockIntro = intros[superblock];
|
||||
|
||||
if (!superBlockIntro) {
|
||||
return helpers.error('any.custom', {
|
||||
message: `Missing intro.json entry for superblock "${superblock}"`
|
||||
});
|
||||
}
|
||||
|
||||
const { error: superBlockError } =
|
||||
superBlockIntroSchema.validate(superBlockIntro);
|
||||
if (superBlockError) {
|
||||
return helpers.error('any.custom', {
|
||||
message: `Invalid intro.json shape for superblock "${superblock}": ${superBlockError.message}`
|
||||
});
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const blockIntro = superBlockIntro.blocks?.[block];
|
||||
if (!blockIntro) {
|
||||
return helpers.error('any.custom', {
|
||||
message: `Missing intro.json block title entry for "${superblock}/${block}"`
|
||||
});
|
||||
}
|
||||
|
||||
const { error: blockError } = blockIntroSchema.validate(blockIntro);
|
||||
if (blockError) {
|
||||
return helpers.error('any.custom', {
|
||||
message: `Invalid intro.json block entry for "${superblock}/${block}": ${blockError.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intros;
|
||||
}, 'intro block coverage validation')
|
||||
.messages({
|
||||
'any.custom': '{{#message}}'
|
||||
});
|
||||
}
|
||||
|
||||
exports.validateIntroSchema = (intros, expectedBlocksBySuperblock) => {
|
||||
return createIntroSchema(expectedBlocksBySuperblock).validate(intros, {
|
||||
abortEarly: false
|
||||
});
|
||||
};
|
||||
53
curriculum/schema/intro-schema.test.mjs
Normal file
53
curriculum/schema/intro-schema.test.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import path from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { addSuperblockStructure } from '../src/build-curriculum.js';
|
||||
import { getCurriculumStructure } from '../src/file-handler.js';
|
||||
import introSchema from './intro-schema.js';
|
||||
|
||||
const { validateIntroSchema } = introSchema;
|
||||
|
||||
const introPath = path.resolve(
|
||||
import.meta.dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
|
||||
const intros = JSON.parse(readFileSync(introPath, 'utf8'));
|
||||
|
||||
function getExpectedBlocksBySuperblock() {
|
||||
const { superblocks } = getCurriculumStructure();
|
||||
|
||||
return addSuperblockStructure(superblocks, true).reduce(
|
||||
(expected, { name, blocks }) => {
|
||||
expected[name] = blocks.map(({ dashedName }) => dashedName);
|
||||
return expected;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
describe('intro schema', () => {
|
||||
it('includes block titles for every curriculum block', () => {
|
||||
const result = validateIntroSchema(intros, getExpectedBlocksBySuperblock());
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if a block title entry is missing', () => {
|
||||
const expectedBlocksBySuperblock = getExpectedBlocksBySuperblock();
|
||||
const [superblock, blocks] = Object.entries(expectedBlocksBySuperblock)[0];
|
||||
const block = blocks[0];
|
||||
const introsWithoutBlock = structuredClone(intros);
|
||||
|
||||
delete introsWithoutBlock[superblock].blocks[block];
|
||||
|
||||
const result = validateIntroSchema(introsWithoutBlock, {
|
||||
[superblock]: [block]
|
||||
});
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.message).toContain(`${superblock}/${block}`);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ const slugRE = new RegExp('^[a-z0-9-]+$');
|
||||
|
||||
const schema = Joi.object()
|
||||
.keys({
|
||||
name: Joi.string().required(),
|
||||
blockLayout: Joi.valid(
|
||||
'challenge-list',
|
||||
'challenge-grid',
|
||||
|
||||
@@ -68,7 +68,6 @@ const dummyUnfinishedSuperBlock = {
|
||||
};
|
||||
|
||||
const dummyBlockMeta = {
|
||||
name: 'Test Block',
|
||||
blockLayout: 'challenge-list',
|
||||
blockLabel: 'workshop',
|
||||
isUpcomingChange: false,
|
||||
@@ -327,7 +326,6 @@ describe('buildSuperblock pure functions', () => {
|
||||
];
|
||||
|
||||
const meta = {
|
||||
name: 'Test Block',
|
||||
dashedName: 'test-block',
|
||||
challengeOrder: [
|
||||
{ id: '1', title: 'Challenge 1' },
|
||||
@@ -346,7 +344,6 @@ describe('buildSuperblock pure functions', () => {
|
||||
const foundChallenges = [{ id: '2', title: 'Challenge 2' }];
|
||||
|
||||
const meta = {
|
||||
name: 'Test Block',
|
||||
dashedName: 'test-block',
|
||||
challengeOrder: [
|
||||
{ id: '1', title: 'Challenge 1' }, // Missing
|
||||
@@ -363,7 +360,6 @@ describe('buildSuperblock pure functions', () => {
|
||||
const foundChallenges = [{ id: '1', title: 'Challenge 1' }];
|
||||
|
||||
const meta = {
|
||||
name: 'Test Block',
|
||||
dashedName: 'test-block',
|
||||
challengeOrder: [{ id: '1', title: 'Challenge 1' }]
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export function validateChallenges(
|
||||
/**
|
||||
* Builds a block object from challenges and meta data
|
||||
* @param {Array<object>} foundChallenges - Array of challenge objects
|
||||
* @param {object} meta - Meta object with name, dashedName, and challengeOrder
|
||||
* @param {object} meta - Meta object with dashedName and challengeOrder
|
||||
* @returns {object} Block object with ordered challenges
|
||||
*/
|
||||
export function buildBlock(foundChallenges: Challenge[], meta: Meta) {
|
||||
@@ -415,7 +415,7 @@ export class BlockCreator {
|
||||
const blockResult = buildBlock(foundChallenges, meta);
|
||||
|
||||
log(
|
||||
`Completed block "${meta.name}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
|
||||
`Completed block "${meta.dashedName}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
|
||||
);
|
||||
|
||||
return blockResult;
|
||||
|
||||
@@ -161,7 +161,6 @@ export type Challenge = {
|
||||
};
|
||||
|
||||
export interface BlockStructure {
|
||||
name: string;
|
||||
hasEditableBoundaries?: boolean;
|
||||
required?: string[];
|
||||
template?: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Add Logic to C# Console Applications",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "add-logic-to-c-sharp-console-applications",
|
||||
"helpCategory": "C-Sharp",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Advanced Node and Express",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "advanced-node-and-express",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Advanced OOP Concepts",
|
||||
"isUpcomingChange": false,
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Advanced Trig & Conics",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Algorithms in Code",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Algorithms",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "algorithms",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Angles and Circular Motion",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Applied Accessibility",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "applied-accessibility",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Applied Visual Design",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "applied-visual-design",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Back-End Development and APIs Projects",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "back-end-development-and-apis-projects",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic Algorithm Scripting",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-algorithm-scripting",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic CSS",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-css",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic Data Structures",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-data-structures",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic HTML and HTML5",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-html-and-html5",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic JavaScript",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-javascript",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic Node and Express",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "basic-node-and-express",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Bootstrap",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "bootstrap",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Budget App Project",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "build-a-budget-app-project",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Cash Register Project",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-cash-register-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Celestial Bodies Database",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-celestial-bodies-database-project",
|
||||
"helpCategory": "Backend Development",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Graph Explorer",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-data-graph-explorer-project",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Financial Calculator",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-financial-calculator-project",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Graphing Calculator",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-graphing-calculator-project",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Multi-Function Calculator",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-multi-function-calculator-project",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Number Guessing Game",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-number-guessing-game-project",
|
||||
"helpCategory": "Backend Development",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Palindrome Checker Project",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-palindrome-checker-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Periodic Table Database",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-periodic-table-database-project",
|
||||
"helpCategory": "Backend Development",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Personal Portfolio Webpage",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-personal-portfolio-webpage-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Polygon Area Calculator Project",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "build-a-polygon-area-calculator-project",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Probability Calculator Project",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "build-a-probability-calculator-project",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Product Landing Page",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-product-landing-page-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Roman Numeral Converter Project",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-roman-numeral-converter-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Salon Appointment Scheduler",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-salon-appointment-scheduler-project",
|
||||
"helpCategory": "Backend Development",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Survey Form",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-survey-form-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Technical Documentation Page",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-technical-documentation-page-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Telephone Number Validator Project",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-telephone-number-validator-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build a Time Calculator Project",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "build-a-time-calculator-project",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Tribute Page",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-tribute-page-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "World Cup Database",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-a-world-cup-database-project",
|
||||
"helpCategory": "Backend Development",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build an Arithmetic Formatter Project",
|
||||
"isUpcomingChange": false,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "build-an-arithmetic-formatter-project",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Build an RPG Creature Search App Project",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-an-rpg-creature-search-app-project",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Three Math Games",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "build-three-math-games-project",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "College Algebra with Python: Conclusion",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "college-algebra-with-python-conclusion",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Control Flow and Functions",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Core Primitives in Python",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Create and Run Simple C# Console Applications",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "create-and-run-simple-c-sharp-console-applications",
|
||||
"helpCategory": "C-Sharp",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Create Methods in C# Console Applications",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "create-methods-in-c-sharp-console-applications",
|
||||
"helpCategory": "C-Sharp",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "CSS Flexbox",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "css-flexbox",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "CSS Grid",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "css-grid",
|
||||
"helpCategory": "HTML-CSS",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Daily Coding Challenges JavaScript",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "daily-coding-challenges-javascript",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Daily Coding Challenges Python",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "daily-coding-challenges-python",
|
||||
"usesMultifileEditor": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Analysis with Python",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "data-analysis-with-python-course",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Analysis with Python Projects",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "data-analysis-with-python-projects",
|
||||
"helpCategory": "Python",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Structures",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "data-structures",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Visualization Projects",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "data-visualization-projects",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Data Visualization with D3",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "data-visualization-with-d3",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Debug C# Console Applications",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "debug-c-sharp-console-applications",
|
||||
"helpCategory": "C-Sharp",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Debugging",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "debugging",
|
||||
"helpCategory": "JavaScript",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Designing Reliable RAG Systems",
|
||||
"isUpcomingChange": false,
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Dictionaries and Loops",
|
||||
"blockLabel": "lecture",
|
||||
"blockLayout": "challenge-list",
|
||||
"isUpcomingChange": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "A2 English for Developers Certification Exam",
|
||||
"isUpcomingChange": false,
|
||||
"dashedName": "en-a2-certification-exam",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Analyzing Code Documentation Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-analyze-code-documentation",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Asking for Code Clarification Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-ask-for-code-clarification",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Basic Programming Vocabulary Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-basic-programming-vocabulary",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Clarifying Information Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-clarify-information-interactions",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Code Concepts and Terms Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-code-related-concepts-terms",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Break Room Conversations Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-conversation-starters-break-room",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Describing Your Current Project Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-describe-current-project",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Daily Routines at Work Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-discuss-morning-evening-routine",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Roles and Responsibilities Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-discuss-roles-responsibilities",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Documenting Code Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-document-code-project",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Educational and Professional Background Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-educational-professional-background",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Expressing Agreement and Disagreement Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-express-agreement-disagreement",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "First Day at The Office Greetings Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-greetings-first-day-office",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Helping a Coworker on GitHub Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-help-coworker-github-troubleshooting",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Online Team Introductions Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-introductions-online-team-meeting",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Offering Technical Support Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-offer-technical-support-guidance",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Technology Trends Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-popular-technology-trends",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Preferences and Motivations Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-preferences-motivations",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Explaining Things to Others Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-provide-explanations-helping-others",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Understanding Code Documentation Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-read-understand-code-documentation",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Requesting and Receiving Guidance Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-request-receive-guidance",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Sharing Progress and Achievements Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-share-progress-accomplishments",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Weekly Meeting Progress Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-share-progress-weekly-meeting",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Talking About Hobbies and Interests Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-talk-about-hobbies-interests",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Talking About Your Workday Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-talk-about-typical-workday-tasks",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Task and Project Updates Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-task-project-updates-plans",
|
||||
"helpCategory": "English",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"name": "Tech Updates and Trends Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "en-a2-quiz-tech-trends-updates",
|
||||
"helpCategory": "English",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user