chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable filenames-simple/naming-convention */
|
||||
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
|
||||
|
||||
export default createLintStagedConfig(import.meta.dirname);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
|
||||
|
||||
export default configTypeChecked;
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "@freecodecamp/shared",
|
||||
"version": "0.0.1",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"license": "BSD-3-Clause",
|
||||
"description": "Config and utils used in multiple workspaces",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown --log-level warn --no-clean --format cjs --format esm",
|
||||
"clean": "rm -rf dist",
|
||||
"develop": "tsdown --log-level warn --no-clean --watch src --format cjs --format esm",
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./config/blocks": {
|
||||
"import": "./dist/config/blocks.mjs",
|
||||
"require": "./dist/config/blocks.cjs"
|
||||
},
|
||||
"./config/catalog": {
|
||||
"import": "./dist/config/catalog.mjs",
|
||||
"require": "./dist/config/catalog.cjs"
|
||||
},
|
||||
"./config/certification-settings": {
|
||||
"import": "./dist/config/certification-settings.mjs",
|
||||
"require": "./dist/config/certification-settings.cjs"
|
||||
},
|
||||
"./config/challenge-types": {
|
||||
"import": "./dist/config/challenge-types.mjs",
|
||||
"require": "./dist/config/challenge-types.cjs"
|
||||
},
|
||||
"./config/chapters": {
|
||||
"import": "./dist/config/chapters.mjs",
|
||||
"require": "./dist/config/chapters.cjs"
|
||||
},
|
||||
"./config/constants": {
|
||||
"import": "./dist/config/constants.mjs",
|
||||
"require": "./dist/config/constants.cjs"
|
||||
},
|
||||
"./config/curriculum": {
|
||||
"import": "./dist/config/curriculum.mjs",
|
||||
"require": "./dist/config/curriculum.cjs"
|
||||
},
|
||||
"./config/donation-settings": {
|
||||
"import": "./dist/config/donation-settings.mjs",
|
||||
"require": "./dist/config/donation-settings.cjs"
|
||||
},
|
||||
"./config/i18n": {
|
||||
"import": "./dist/config/i18n.mjs",
|
||||
"require": "./dist/config/i18n.cjs"
|
||||
},
|
||||
"./config/modules": {
|
||||
"import": "./dist/config/modules.mjs",
|
||||
"require": "./dist/config/modules.cjs"
|
||||
},
|
||||
"./utils/get-lines": {
|
||||
"import": "./dist/utils/get-lines.mjs",
|
||||
"require": "./dist/utils/get-lines.cjs"
|
||||
},
|
||||
"./utils/is-audited": {
|
||||
"import": "./dist/utils/is-audited.mjs",
|
||||
"require": "./dist/utils/is-audited.cjs"
|
||||
},
|
||||
"./utils/polyvinyl": {
|
||||
"import": "./dist/utils/polyvinyl.mjs",
|
||||
"require": "./dist/utils/polyvinyl.cjs"
|
||||
},
|
||||
"./utils/shuffle-array": {
|
||||
"import": "./dist/utils/shuffle-array.mjs",
|
||||
"require": "./dist/utils/shuffle-array.cjs"
|
||||
},
|
||||
"./utils/validate": {
|
||||
"import": "./dist/utils/validate.mjs",
|
||||
"require": "./dist/utils/validate.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"devDependencies": {
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"eslint": "^9.39.1",
|
||||
"tsdown": "^0.21.0",
|
||||
"vitest": "^4.0.15"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export enum BlockLabel {
|
||||
lecture = 'lecture',
|
||||
workshop = 'workshop',
|
||||
lab = 'lab',
|
||||
review = 'review',
|
||||
quiz = 'quiz',
|
||||
exam = 'exam',
|
||||
certProject = 'cert-project',
|
||||
/* The tags below refer to the Language Curricula chapter based certifications*/
|
||||
warmup = 'warm-up',
|
||||
learn = 'learn',
|
||||
practice = 'practice'
|
||||
}
|
||||
|
||||
export enum BlockLayouts {
|
||||
/**
|
||||
* These three are for the new Full Stack Developer Certification,
|
||||
* so we can play with them without affecting the existing block layouts
|
||||
*/
|
||||
ChallengeList = 'challenge-list',
|
||||
Link = 'link',
|
||||
ChallengeGrid = 'challenge-grid',
|
||||
|
||||
DialogueGrid = 'dialogue-grid',
|
||||
|
||||
/**
|
||||
* ChallengeList displays challenges in a list.
|
||||
* This layout is used in backend blocks, The Odin Project blocks, and blocks in legacy certification.
|
||||
* Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#basic-javascript
|
||||
*/
|
||||
LegacyChallengeList = 'legacy-challenge-list',
|
||||
|
||||
/**
|
||||
* LegacyChallengeGrid displays challenges in a grid.
|
||||
* This layout is used for step-based blocks.
|
||||
* Example: https://www.freecodecamp.org/learn/2022/responsive-web-design/#learn-html-by-building-a-cat-photo-app
|
||||
*/
|
||||
LegacyChallengeGrid = 'legacy-challenge-grid',
|
||||
|
||||
/**
|
||||
* Link displays the block as a single link.
|
||||
* This layout is used if the block has a single challenge.
|
||||
* Example: https://www.freecodecamp.org/learn/2022/responsive-web-design/#build-a-survey-form-project
|
||||
*/
|
||||
LegacyLink = 'legacy-link',
|
||||
|
||||
/**
|
||||
* ProjectList displays a list of certification projects.
|
||||
* This layout is used in legacy certifications.
|
||||
* Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#javascript-algorithms-and-data-structures-projects
|
||||
*/
|
||||
ProjectList = 'project-list'
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { catalog } from './catalog';
|
||||
import { superBlockStages, SuperBlockStage } from './curriculum';
|
||||
|
||||
describe('catalog', () => {
|
||||
it('should have exactly one entry for each superblock in SuperBlockStage.Catalog', () => {
|
||||
const catalogSuperBlocks = superBlockStages[SuperBlockStage.Catalog];
|
||||
|
||||
expect(catalog.map(course => course.superBlock.toString()).sort()).toEqual(
|
||||
catalogSuperBlocks.map(sb => sb.toString()).sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,416 @@
|
||||
import { SuperBlocks } from './curriculum';
|
||||
|
||||
enum Levels {
|
||||
Beginner = 'beginner',
|
||||
Intermediate = 'intermediate',
|
||||
Advanced = 'advanced'
|
||||
}
|
||||
|
||||
enum Topic {
|
||||
Html = 'html',
|
||||
CSS = 'css',
|
||||
Js = 'js',
|
||||
React = 'react',
|
||||
Python = 'python',
|
||||
DataAnalysis = 'data-analysis',
|
||||
MachineLearning = 'machine-learning',
|
||||
D3 = 'd3',
|
||||
Api = 'api',
|
||||
InformationSecurity = 'information-security',
|
||||
ComputerFundamentals = 'computer-fundamentals',
|
||||
ComputerScience = 'computer-science',
|
||||
Math = 'math',
|
||||
Databases = 'databases',
|
||||
Bash = 'bash',
|
||||
Git = 'git',
|
||||
Editors = 'editors',
|
||||
AI = 'ai'
|
||||
}
|
||||
|
||||
interface Catalog {
|
||||
superBlock: SuperBlocks;
|
||||
level: Levels;
|
||||
hours: number;
|
||||
topic: Topic;
|
||||
}
|
||||
|
||||
export const catalog: Catalog[] = [
|
||||
{
|
||||
superBlock: SuperBlocks.LearnPythonForBeginners,
|
||||
level: Levels.Beginner,
|
||||
hours: 5,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToAlgorithmsAndDataStructures,
|
||||
level: Levels.Intermediate,
|
||||
hours: 6,
|
||||
topic: Topic.ComputerScience
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnRAGAndMCPFundamentals,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.AI
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.SemanticHtml,
|
||||
level: Levels.Beginner,
|
||||
hours: 2,
|
||||
topic: Topic.Html
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.ComputerBasics,
|
||||
level: Levels.Beginner,
|
||||
hours: 2,
|
||||
topic: Topic.ComputerFundamentals
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.BasicCss,
|
||||
level: Levels.Beginner,
|
||||
hours: 3,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.BasicHtml,
|
||||
level: Levels.Beginner,
|
||||
hours: 3,
|
||||
topic: Topic.Html
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.DesignForDevelopers,
|
||||
level: Levels.Beginner,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.HtmlAndAccessibility,
|
||||
level: Levels.Beginner,
|
||||
hours: 2,
|
||||
topic: Topic.Html
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssFlexbox,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToPrecalculus,
|
||||
level: Levels.Intermediate,
|
||||
hours: 6,
|
||||
topic: Topic.Math
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToGitAndGithub,
|
||||
level: Levels.Intermediate,
|
||||
hours: 20,
|
||||
topic: Topic.Git
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToPythonBasics,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.AbsoluteAndRelativeUnits,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.PseudoClassesAndElements,
|
||||
level: Levels.Intermediate,
|
||||
hours: 1,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.HtmlFormsAndTables,
|
||||
level: Levels.Beginner,
|
||||
hours: 2,
|
||||
topic: Topic.Html
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssColors,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.JavascriptFundamentalsReview,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.StylingForms,
|
||||
level: Levels.Intermediate,
|
||||
hours: 1,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssBoxModel,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToBash,
|
||||
level: Levels.Intermediate,
|
||||
hours: 20,
|
||||
topic: Topic.Bash
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToLinearDataStructuresInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnAlgorithmsInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssTypography,
|
||||
level: Levels.Intermediate,
|
||||
hours: 1,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssAndAccessibility,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssPositioning,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.AttributeSelectors,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.ResponsiveDesign,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssVariables,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssGrid,
|
||||
level: Levels.Intermediate,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.CssAnimations,
|
||||
level: Levels.Advanced,
|
||||
hours: 2,
|
||||
topic: Topic.CSS
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToSQLAndPostgreSQL,
|
||||
level: Levels.Intermediate,
|
||||
hours: 30,
|
||||
topic: Topic.Databases
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnBashScripting,
|
||||
level: Levels.Intermediate,
|
||||
hours: 20,
|
||||
topic: Topic.Bash
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnSQLAndBash,
|
||||
level: Levels.Intermediate,
|
||||
hours: 30,
|
||||
topic: Topic.Databases
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToNano,
|
||||
level: Levels.Intermediate,
|
||||
hours: 10,
|
||||
topic: Topic.Editors
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToVariablesAndStringsInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToBooleansAndNumbersInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToFunctionsInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToArraysInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToObjectsInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToLoopsInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnDomManipulationAndEventsWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToJavascriptAndAccessibility,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnJavascriptDebugging,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnBasicRegexWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToDatesInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnAudioAndVideoEventsWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToMapsAndSetsInJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToJavascriptClasses,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnRecursionWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToFunctionalProgrammingWithJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToAsynchronousJS,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Js
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnDataVisualizationWithD3,
|
||||
level: Levels.Intermediate,
|
||||
hours: 20,
|
||||
topic: Topic.D3
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnOOPWithPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 3,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnPythonLoopsAndSequences,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnPythonDictionariesAndSets,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnErrorHandlingInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnPythonClassesAndObjects,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.IntroductionToOOPInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnGraphsAndTreesInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
},
|
||||
{
|
||||
superBlock: SuperBlocks.LearnDynamicProgrammingInPython,
|
||||
level: Levels.Intermediate,
|
||||
hours: 40,
|
||||
topic: Topic.Python
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
Certification,
|
||||
linkedInCredentialIds,
|
||||
certToTitleMap,
|
||||
certToIdMap,
|
||||
certSlugTypeMap,
|
||||
isCertified
|
||||
} from './certification-settings';
|
||||
|
||||
describe('linkedInCredentialIds', () => {
|
||||
it('should contain a value for all certifications', () => {
|
||||
const allCertifications = Object.values(Certification).sort();
|
||||
const linkedInCredentialIdsKeys = Object.keys(linkedInCredentialIds).sort();
|
||||
|
||||
expect(linkedInCredentialIdsKeys).toEqual(allCertifications);
|
||||
});
|
||||
});
|
||||
|
||||
describe('certToTitleMap', () => {
|
||||
it('should not contain duplicate titles', () => {
|
||||
const titles = Object.values(certToTitleMap);
|
||||
const uniqueTitles = Array.from(new Set(titles));
|
||||
|
||||
expect(titles.length).toBe(uniqueTitles.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('certToIdMap', () => {
|
||||
it('should have no duplicate values', () => {
|
||||
const ids = Object.values(certToIdMap).sort();
|
||||
const uniqueIds = Array.from(new Set(ids)).sort();
|
||||
|
||||
expect(uniqueIds).toEqual(ids);
|
||||
});
|
||||
});
|
||||
|
||||
describe('certSlugTypeMap', () => {
|
||||
it('should contain a value for all certifications', () => {
|
||||
const allCertifications = Object.values(Certification).sort();
|
||||
const certSlugTypeMapKeys = Object.keys(certSlugTypeMap).sort();
|
||||
|
||||
expect(certSlugTypeMapKeys).toEqual(allCertifications);
|
||||
});
|
||||
|
||||
it('should have no duplicate values', () => {
|
||||
const types = Object.values(certSlugTypeMap).sort();
|
||||
const uniqueTypes = Array.from(new Set(types)).sort();
|
||||
|
||||
expect(uniqueTypes).toEqual(types);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCertified', () => {
|
||||
it('should return true if a user has the specified certification', () => {
|
||||
const cert = Certification.RespWebDesignV9;
|
||||
const user = {
|
||||
isRespWebDesignCertV9: true
|
||||
};
|
||||
|
||||
expect(isCertified(user, cert)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if a user does not have the specified certification', () => {
|
||||
const cert = Certification.JsAlgoDataStruct;
|
||||
const user = {
|
||||
isRespWebDesignCertV9: true
|
||||
};
|
||||
|
||||
expect(isCertified(user, cert)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the certification does not exist', () => {
|
||||
const cert = 'NonExistentCert' as Certification;
|
||||
const user = {
|
||||
isRespWebDesignCertV9: true
|
||||
};
|
||||
|
||||
expect(isCertified(user, cert)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,457 @@
|
||||
import { SuperBlocks } from '../config/curriculum.js';
|
||||
|
||||
/**
|
||||
* Certifications are not equivalent to superblocks. Each superblock corresponds
|
||||
* to 0 or 1 certifications, but a certification may not correspond to a
|
||||
* superblock.
|
||||
*
|
||||
* As an example of the former: the CodingInterviewPrep superblock does not
|
||||
* correspond to a certification. As an example of the latter: the legacy
|
||||
* front-end certification no longer has a corresponding superblock.
|
||||
*
|
||||
* The value of each enum member is the slug of the corresponding certification.
|
||||
*/
|
||||
export enum Certification {
|
||||
RespWebDesign = 'responsive-web-design',
|
||||
JsAlgoDataStructNew = 'javascript-algorithms-and-data-structures-v8',
|
||||
FrontEndDevLibs = 'front-end-development-libraries',
|
||||
DataVis = 'data-visualization',
|
||||
RelationalDb = 'relational-database-v8',
|
||||
BackEndDevApis = 'back-end-development-and-apis',
|
||||
QualityAssurance = 'quality-assurance-v7',
|
||||
SciCompPy = 'scientific-computing-with-python-v7',
|
||||
DataAnalysisPy = 'data-analysis-with-python-v7',
|
||||
InfoSec = 'information-security-v7',
|
||||
MachineLearningPy = 'machine-learning-with-python-v7',
|
||||
CollegeAlgebraPy = 'college-algebra-with-python-v8',
|
||||
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
|
||||
PythonV9 = 'python-v9',
|
||||
RelationalDbV9 = 'relational-databases-v9',
|
||||
// Upcoming certifications
|
||||
RespWebDesignV9 = 'responsive-web-design-v9',
|
||||
JsV9 = 'javascript-v9',
|
||||
FrontEndDevLibsV9 = 'front-end-development-libraries-v9',
|
||||
BackEndDevApisV9 = 'back-end-development-and-apis-v9',
|
||||
A2English = 'a2-english-for-developers',
|
||||
FullStackDeveloperV9 = 'full-stack-developer-v9',
|
||||
B1English = 'b1-english-for-developers',
|
||||
A2Spanish = 'a2-professional-spanish',
|
||||
A2Chinese = 'a2-professional-chinese',
|
||||
A1Chinese = 'a1-professional-chinese',
|
||||
// Legacy certifications
|
||||
LegacyFrontEnd = 'legacy-front-end',
|
||||
JsAlgoDataStruct = 'javascript-algorithms-and-data-structures',
|
||||
LegacyBackEnd = 'legacy-back-end',
|
||||
LegacyDataVis = 'legacy-data-visualization',
|
||||
LegacyInfoSecQa = 'information-security-and-quality-assurance',
|
||||
LegacyFullStack = 'full-stack'
|
||||
}
|
||||
|
||||
export function isCertification(x: string): x is Certification {
|
||||
return Object.values(Certification).includes(x as Certification);
|
||||
}
|
||||
|
||||
// "Current" certifications are the subset of standard certifications that are
|
||||
// live and not legacy.
|
||||
export const currentCertifications = [
|
||||
Certification.A2English,
|
||||
Certification.B1English,
|
||||
Certification.FoundationalCSharp,
|
||||
Certification.JsV9,
|
||||
Certification.PythonV9,
|
||||
Certification.RelationalDbV9,
|
||||
Certification.RespWebDesignV9
|
||||
] as const;
|
||||
|
||||
// "Legacy" certifications are another class of standard certifications. They're
|
||||
// still live and claimable, but some parts of the UI handle them differently.
|
||||
export const legacyCertifications = [
|
||||
Certification.RespWebDesign,
|
||||
Certification.JsAlgoDataStruct,
|
||||
Certification.FrontEndDevLibs,
|
||||
Certification.DataVis,
|
||||
Certification.BackEndDevApis,
|
||||
Certification.LegacyInfoSecQa,
|
||||
Certification.LegacyFrontEnd,
|
||||
Certification.JsAlgoDataStructNew,
|
||||
Certification.LegacyBackEnd,
|
||||
Certification.LegacyDataVis,
|
||||
Certification.RelationalDb,
|
||||
Certification.QualityAssurance,
|
||||
Certification.SciCompPy,
|
||||
Certification.DataAnalysisPy,
|
||||
Certification.InfoSec,
|
||||
Certification.MachineLearningPy,
|
||||
Certification.CollegeAlgebraPy
|
||||
] as const;
|
||||
|
||||
// The Legacy Full Stack certification can only be claimed when specific
|
||||
// "current" and "legacy" certifications have been claimed.
|
||||
export const legacyFullStackCertification = [
|
||||
Certification.LegacyFullStack
|
||||
] as const;
|
||||
|
||||
// "Upcoming" certifications are standard certifications that are not live unless
|
||||
// showUpcomingChanges is true.
|
||||
export const upcomingCertifications = [
|
||||
Certification.FrontEndDevLibsV9,
|
||||
Certification.BackEndDevApisV9,
|
||||
Certification.FullStackDeveloperV9,
|
||||
Certification.A2Spanish,
|
||||
Certification.A2Chinese,
|
||||
Certification.A1Chinese
|
||||
] as const;
|
||||
|
||||
export const certToIdMap: Record<Certification, string> = {
|
||||
// Legacy certifications
|
||||
[Certification.LegacyFrontEnd]: '561add10cb82ac38a17513be',
|
||||
[Certification.JsAlgoDataStruct]: '561abd10cb81ac38a17513bc',
|
||||
[Certification.LegacyBackEnd]: '660add10cb82ac38a17513be',
|
||||
[Certification.LegacyDataVis]: '561add10cb82ac39a17513bc',
|
||||
[Certification.LegacyInfoSecQa]: '561add10cb82ac38a17213bc',
|
||||
[Certification.LegacyFullStack]: '561add10cb82ac38a17213bd',
|
||||
|
||||
// Current certifications
|
||||
[Certification.RespWebDesign]: '561add10cb82ac38a17513bc',
|
||||
[Certification.JsAlgoDataStructNew]: '658180220947283cdc0689ce',
|
||||
[Certification.FrontEndDevLibs]: '561acd10cb82ac38a17513bc',
|
||||
[Certification.DataVis]: '5a553ca864b52e1d8bceea14',
|
||||
[Certification.BackEndDevApis]: '561add10cb82ac38a17523bc',
|
||||
[Certification.QualityAssurance]: '5e611829481575a52dc59c0e',
|
||||
[Certification.InfoSec]: '5e6021435ac9d0ecd8b94b00',
|
||||
[Certification.SciCompPy]: '5e44431b903586ffb414c951',
|
||||
[Certification.DataAnalysisPy]: '5e46fc95ac417301a38fb934',
|
||||
[Certification.MachineLearningPy]: '5e46fc95ac417301a38fb935',
|
||||
[Certification.RelationalDb]: '606243f50267e718b1e755f4',
|
||||
[Certification.CollegeAlgebraPy]: '61531b20cc9dfa2741a5b800',
|
||||
[Certification.FoundationalCSharp]: '647f7da207d29547b3bee1ba',
|
||||
[Certification.A2English]: '651dd7e01d697d0aab7833b7',
|
||||
|
||||
// Upcoming certifications
|
||||
[Certification.RespWebDesignV9]: '68db314d3c11a8bff07c7535',
|
||||
[Certification.JsV9]: '68c4069c1ef859270e17c495',
|
||||
[Certification.FrontEndDevLibsV9]: '68e008aa5f80c6099d47b3a2',
|
||||
[Certification.PythonV9]: '68e6bd5020effa1586e79855',
|
||||
[Certification.RelationalDbV9]: '68e6bd5120effa1586e79856',
|
||||
[Certification.BackEndDevApisV9]: '68e6bd5120effa1586e79857',
|
||||
[Certification.FullStackDeveloperV9]: '64514fda6c245de4d11eb7bb',
|
||||
[Certification.B1English]: '66607e53317411dd5e8aae21',
|
||||
[Certification.A2Spanish]: '681a6b22e5a782fe3459984a',
|
||||
[Certification.A2Chinese]: '682c3153086dd7cabe7f48bc',
|
||||
[Certification.A1Chinese]: '68f1268149f045a650d4229e'
|
||||
};
|
||||
|
||||
export const completionHours: Record<Certification, number> = {
|
||||
[Certification.LegacyFrontEnd]: 300,
|
||||
[Certification.JsAlgoDataStruct]: 300,
|
||||
[Certification.LegacyBackEnd]: 300,
|
||||
[Certification.LegacyDataVis]: 300,
|
||||
[Certification.LegacyInfoSecQa]: 300,
|
||||
[Certification.LegacyFullStack]: 1800,
|
||||
[Certification.RespWebDesign]: 300,
|
||||
[Certification.JsAlgoDataStructNew]: 300,
|
||||
[Certification.FrontEndDevLibs]: 300,
|
||||
[Certification.DataVis]: 300,
|
||||
[Certification.BackEndDevApis]: 300,
|
||||
[Certification.QualityAssurance]: 300,
|
||||
[Certification.InfoSec]: 300,
|
||||
[Certification.SciCompPy]: 300,
|
||||
[Certification.DataAnalysisPy]: 300,
|
||||
[Certification.MachineLearningPy]: 300,
|
||||
[Certification.RelationalDb]: 300,
|
||||
[Certification.CollegeAlgebraPy]: 300,
|
||||
[Certification.FoundationalCSharp]: 300,
|
||||
[Certification.A2English]: 300,
|
||||
[Certification.RespWebDesignV9]: 300,
|
||||
[Certification.JsV9]: 300,
|
||||
[Certification.FrontEndDevLibsV9]: 300,
|
||||
[Certification.PythonV9]: 300,
|
||||
[Certification.RelationalDbV9]: 300,
|
||||
[Certification.BackEndDevApisV9]: 300,
|
||||
[Certification.FullStackDeveloperV9]: 1800,
|
||||
[Certification.B1English]: 300,
|
||||
[Certification.A2Spanish]: 300,
|
||||
[Certification.A2Chinese]: 300,
|
||||
[Certification.A1Chinese]: 300
|
||||
};
|
||||
|
||||
type UserCertFlag =
|
||||
| 'isFrontEndCert'
|
||||
| 'isJsAlgoDataStructCert'
|
||||
| 'isBackEndCert'
|
||||
| 'isDataVisCert'
|
||||
| 'isInfosecQaCert'
|
||||
| 'isFullStackCert'
|
||||
| 'isRespWebDesignCert'
|
||||
| 'isJsAlgoDataStructCertV8'
|
||||
| 'isFrontEndLibsCert'
|
||||
| 'is2018DataVisCert'
|
||||
| 'isApisMicroservicesCert'
|
||||
| 'isQaCertV7'
|
||||
| 'isInfosecCertV7'
|
||||
| 'isSciCompPyCertV7'
|
||||
| 'isDataAnalysisPyCertV7'
|
||||
| 'isMachineLearningPyCertV7'
|
||||
| 'isRelationalDatabaseCertV8'
|
||||
| 'isCollegeAlgebraPyCertV8'
|
||||
| 'isFoundationalCSharpCertV8'
|
||||
| 'isA2EnglishCert'
|
||||
| 'isRespWebDesignCertV9'
|
||||
| 'isJavascriptCertV9'
|
||||
| 'isFrontEndLibsCertV9'
|
||||
| 'isPythonCertV9'
|
||||
| 'isRelationalDatabaseCertV9'
|
||||
| 'isBackEndDevApisCertV9'
|
||||
| 'isFullStackDeveloperCertV9'
|
||||
| 'isB1EnglishCert'
|
||||
| 'isA2SpanishCert'
|
||||
| 'isA2ChineseCert'
|
||||
| 'isA1ChineseCert';
|
||||
|
||||
export const certSlugTypeMap: Record<Certification, UserCertFlag> = {
|
||||
// legacy
|
||||
[Certification.LegacyFrontEnd]: 'isFrontEndCert',
|
||||
[Certification.JsAlgoDataStruct]: 'isJsAlgoDataStructCert',
|
||||
[Certification.LegacyBackEnd]: 'isBackEndCert',
|
||||
[Certification.LegacyDataVis]: 'isDataVisCert',
|
||||
[Certification.LegacyInfoSecQa]: 'isInfosecQaCert',
|
||||
[Certification.LegacyFullStack]: 'isFullStackCert',
|
||||
|
||||
// modern
|
||||
[Certification.RespWebDesign]: 'isRespWebDesignCert',
|
||||
[Certification.JsAlgoDataStructNew]: 'isJsAlgoDataStructCertV8',
|
||||
[Certification.FrontEndDevLibs]: 'isFrontEndLibsCert',
|
||||
[Certification.DataVis]: 'is2018DataVisCert',
|
||||
[Certification.BackEndDevApis]: 'isApisMicroservicesCert',
|
||||
[Certification.QualityAssurance]: 'isQaCertV7',
|
||||
[Certification.InfoSec]: 'isInfosecCertV7',
|
||||
[Certification.SciCompPy]: 'isSciCompPyCertV7',
|
||||
[Certification.DataAnalysisPy]: 'isDataAnalysisPyCertV7',
|
||||
[Certification.MachineLearningPy]: 'isMachineLearningPyCertV7',
|
||||
[Certification.RelationalDb]: 'isRelationalDatabaseCertV8',
|
||||
[Certification.CollegeAlgebraPy]: 'isCollegeAlgebraPyCertV8',
|
||||
[Certification.FoundationalCSharp]: 'isFoundationalCSharpCertV8',
|
||||
[Certification.A2English]: 'isA2EnglishCert',
|
||||
[Certification.PythonV9]: 'isPythonCertV9',
|
||||
[Certification.RelationalDbV9]: 'isRelationalDatabaseCertV9',
|
||||
[Certification.RespWebDesignV9]: 'isRespWebDesignCertV9',
|
||||
[Certification.JsV9]: 'isJavascriptCertV9',
|
||||
|
||||
// upcoming
|
||||
[Certification.FrontEndDevLibsV9]: 'isFrontEndLibsCertV9',
|
||||
[Certification.BackEndDevApisV9]: 'isBackEndDevApisCertV9',
|
||||
[Certification.FullStackDeveloperV9]: 'isFullStackDeveloperCertV9',
|
||||
[Certification.B1English]: 'isB1EnglishCert',
|
||||
[Certification.A2Spanish]: 'isA2SpanishCert',
|
||||
[Certification.A2Chinese]: 'isA2ChineseCert',
|
||||
[Certification.A1Chinese]: 'isA1ChineseCert'
|
||||
};
|
||||
|
||||
export type CertificationFlags = {
|
||||
[key in UserCertFlag]: boolean;
|
||||
};
|
||||
|
||||
export function isCertified(
|
||||
user: Partial<CertificationFlags>,
|
||||
cert: Certification
|
||||
): boolean {
|
||||
const certFlag = certSlugTypeMap[cert];
|
||||
return Boolean(user[certFlag]);
|
||||
}
|
||||
|
||||
// TODO: use i18n keys instead of hardcoded titles
|
||||
export const certToTitleMap: Record<Certification, string> = {
|
||||
// Legacy certifications
|
||||
[Certification.LegacyFrontEnd]: 'Legacy Frontend',
|
||||
[Certification.JsAlgoDataStruct]:
|
||||
'Legacy JavaScript Algorithms and Data Structures V7',
|
||||
[Certification.LegacyBackEnd]: 'Legacy Backend',
|
||||
[Certification.LegacyDataVis]: 'Legacy Data Visualization',
|
||||
[Certification.LegacyInfoSecQa]:
|
||||
'Legacy Information Security and Quality Assurance',
|
||||
[Certification.LegacyFullStack]: 'Legacy Full-Stack',
|
||||
|
||||
// Current certifications
|
||||
[Certification.RespWebDesign]: 'Legacy Responsive Web Design V8',
|
||||
[Certification.JsAlgoDataStructNew]:
|
||||
'Legacy JavaScript Algorithms and Data Structures V8',
|
||||
[Certification.FrontEndDevLibs]: 'Frontend Development Libraries V8',
|
||||
[Certification.DataVis]: 'Data Visualization V8',
|
||||
[Certification.BackEndDevApis]: 'Backend Development and APIs V8',
|
||||
[Certification.QualityAssurance]: 'Quality Assurance',
|
||||
[Certification.InfoSec]: 'Information Security',
|
||||
[Certification.SciCompPy]: 'Scientific Computing with Python',
|
||||
[Certification.DataAnalysisPy]: 'Data Analysis with Python',
|
||||
[Certification.MachineLearningPy]: 'Machine Learning with Python',
|
||||
[Certification.RelationalDb]: 'Relational Database V8',
|
||||
[Certification.CollegeAlgebraPy]: 'College Algebra with Python',
|
||||
[Certification.FoundationalCSharp]: 'Foundational C# with Microsoft',
|
||||
[Certification.A2English]: 'A2 English for Developers',
|
||||
|
||||
// Upcoming certifications
|
||||
[Certification.RespWebDesignV9]: 'Responsive Web Design',
|
||||
[Certification.JsV9]: 'JavaScript',
|
||||
[Certification.FrontEndDevLibsV9]: 'Frontend Development Libraries',
|
||||
[Certification.PythonV9]: 'Python',
|
||||
[Certification.RelationalDbV9]: 'Relational Database',
|
||||
[Certification.BackEndDevApisV9]: 'Backend Development and APIs',
|
||||
[Certification.FullStackDeveloperV9]: 'Full-Stack Developer',
|
||||
[Certification.B1English]: 'B1 English for Developers',
|
||||
[Certification.A2Spanish]: 'A2 Professional Spanish',
|
||||
[Certification.A2Chinese]: 'A2 Professional Chinese',
|
||||
[Certification.A1Chinese]: 'A1 Professional Chinese'
|
||||
};
|
||||
|
||||
export const superBlockToCertMap: {
|
||||
[key in SuperBlocks]: Certification | null;
|
||||
} = {
|
||||
[SuperBlocks.RespWebDesign]: Certification.RespWebDesign,
|
||||
[SuperBlocks.JsAlgoDataStructNew]: Certification.JsAlgoDataStructNew,
|
||||
[SuperBlocks.FrontEndDevLibs]: Certification.FrontEndDevLibs,
|
||||
[SuperBlocks.DataVis]: Certification.DataVis,
|
||||
[SuperBlocks.RelationalDb]: Certification.RelationalDb,
|
||||
[SuperBlocks.BackEndDevApis]: Certification.BackEndDevApis,
|
||||
[SuperBlocks.QualityAssurance]: Certification.QualityAssurance,
|
||||
[SuperBlocks.SciCompPy]: Certification.SciCompPy,
|
||||
[SuperBlocks.DataAnalysisPy]: Certification.DataAnalysisPy,
|
||||
[SuperBlocks.InfoSec]: Certification.InfoSec,
|
||||
[SuperBlocks.MachineLearningPy]: Certification.MachineLearningPy,
|
||||
[SuperBlocks.CollegeAlgebraPy]: Certification.CollegeAlgebraPy,
|
||||
[SuperBlocks.FoundationalCSharp]: Certification.FoundationalCSharp,
|
||||
[SuperBlocks.RespWebDesignNew]: Certification.RespWebDesign,
|
||||
[SuperBlocks.JsAlgoDataStruct]: Certification.JsAlgoDataStruct,
|
||||
[SuperBlocks.RespWebDesignV9]: Certification.RespWebDesignV9,
|
||||
[SuperBlocks.JsV9]: Certification.JsV9,
|
||||
[SuperBlocks.FrontEndDevLibsV9]: Certification.FrontEndDevLibsV9,
|
||||
[SuperBlocks.PythonV9]: Certification.PythonV9,
|
||||
[SuperBlocks.RelationalDbV9]: Certification.RelationalDbV9,
|
||||
[SuperBlocks.BackEndDevApisV9]: Certification.BackEndDevApisV9,
|
||||
[SuperBlocks.FullStackDeveloperV9]: Certification.FullStackDeveloperV9,
|
||||
[SuperBlocks.A2English]: Certification.A2English,
|
||||
[SuperBlocks.B1English]: Certification.B1English,
|
||||
[SuperBlocks.A1Spanish]: null,
|
||||
[SuperBlocks.A2Spanish]: Certification.A2Spanish,
|
||||
[SuperBlocks.A2Chinese]: Certification.A2Chinese,
|
||||
[SuperBlocks.A1Chinese]: Certification.A1Chinese,
|
||||
[SuperBlocks.PythonForEverybody]: null,
|
||||
[SuperBlocks.CodingInterviewPrep]: null,
|
||||
[SuperBlocks.ProjectEuler]: null,
|
||||
[SuperBlocks.TheOdinProject]: null,
|
||||
[SuperBlocks.RosettaCode]: null,
|
||||
[SuperBlocks.BasicHtml]: null,
|
||||
[SuperBlocks.SemanticHtml]: null,
|
||||
[SuperBlocks.DevPlayground]: null,
|
||||
[SuperBlocks.FullStackOpen]: null,
|
||||
[SuperBlocks.HtmlFormsAndTables]: null,
|
||||
[SuperBlocks.HtmlAndAccessibility]: null,
|
||||
[SuperBlocks.ComputerBasics]: null,
|
||||
[SuperBlocks.BasicCss]: null,
|
||||
[SuperBlocks.DesignForDevelopers]: null,
|
||||
[SuperBlocks.AbsoluteAndRelativeUnits]: null,
|
||||
[SuperBlocks.PseudoClassesAndElements]: null,
|
||||
[SuperBlocks.CssColors]: null,
|
||||
[SuperBlocks.StylingForms]: null,
|
||||
[SuperBlocks.CssBoxModel]: null,
|
||||
[SuperBlocks.CssFlexbox]: null,
|
||||
[SuperBlocks.CssTypography]: null,
|
||||
[SuperBlocks.CssAndAccessibility]: null,
|
||||
[SuperBlocks.CssPositioning]: null,
|
||||
[SuperBlocks.AttributeSelectors]: null,
|
||||
[SuperBlocks.ResponsiveDesign]: null,
|
||||
[SuperBlocks.CssVariables]: null,
|
||||
[SuperBlocks.CssGrid]: null,
|
||||
[SuperBlocks.CssAnimations]: null,
|
||||
[SuperBlocks.LearnPythonForBeginners]: null,
|
||||
[SuperBlocks.IntroductionToAlgorithmsAndDataStructures]: null,
|
||||
[SuperBlocks.LearnOOPWithPython]: null,
|
||||
[SuperBlocks.LearnRAGAndMCPFundamentals]: null,
|
||||
[SuperBlocks.IntroductionToPrecalculus]: null,
|
||||
[SuperBlocks.IntroductionToBash]: null,
|
||||
[SuperBlocks.IntroductionToSQLAndPostgreSQL]: null,
|
||||
[SuperBlocks.LearnBashScripting]: null,
|
||||
[SuperBlocks.LearnSQLAndBash]: null,
|
||||
[SuperBlocks.IntroductionToNano]: null,
|
||||
[SuperBlocks.IntroductionToGitAndGithub]: null,
|
||||
[SuperBlocks.IntroductionToVariablesAndStringsInJS]: null,
|
||||
[SuperBlocks.IntroductionToBooleansAndNumbersInJS]: null,
|
||||
[SuperBlocks.IntroductionToFunctionsInJS]: null,
|
||||
[SuperBlocks.IntroductionToArraysInJS]: null,
|
||||
[SuperBlocks.IntroductionToObjectsInJS]: null,
|
||||
[SuperBlocks.IntroductionToLoopsInJS]: null,
|
||||
[SuperBlocks.JavascriptFundamentalsReview]: null,
|
||||
[SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS]: null,
|
||||
[SuperBlocks.LearnDomManipulationAndEventsWithJS]: null,
|
||||
[SuperBlocks.IntroductionToJavascriptAndAccessibility]: null,
|
||||
[SuperBlocks.LearnJavascriptDebugging]: null,
|
||||
[SuperBlocks.LearnBasicRegexWithJS]: null,
|
||||
[SuperBlocks.IntroductionToDatesInJS]: null,
|
||||
[SuperBlocks.LearnAudioAndVideoEventsWithJS]: null,
|
||||
[SuperBlocks.IntroductionToMapsAndSetsInJS]: null,
|
||||
[SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS]: null,
|
||||
[SuperBlocks.IntroductionToJavascriptClasses]: null,
|
||||
[SuperBlocks.LearnRecursionWithJS]: null,
|
||||
[SuperBlocks.IntroductionToFunctionalProgrammingWithJS]: null,
|
||||
[SuperBlocks.IntroductionToAsynchronousJS]: null,
|
||||
[SuperBlocks.LearnDataVisualizationWithD3]: null,
|
||||
[SuperBlocks.IntroductionToPythonBasics]: null,
|
||||
[SuperBlocks.LearnPythonLoopsAndSequences]: null,
|
||||
[SuperBlocks.LearnPythonDictionariesAndSets]: null,
|
||||
[SuperBlocks.LearnErrorHandlingInPython]: null,
|
||||
[SuperBlocks.LearnPythonClassesAndObjects]: null,
|
||||
[SuperBlocks.IntroductionToOOPInPython]: null,
|
||||
[SuperBlocks.IntroductionToLinearDataStructuresInPython]: null,
|
||||
[SuperBlocks.LearnAlgorithmsInPython]: null,
|
||||
[SuperBlocks.LearnGraphsAndTreesInPython]: null,
|
||||
[SuperBlocks.LearnDynamicProgrammingInPython]: null
|
||||
};
|
||||
|
||||
export const certificationRequirements: Partial<
|
||||
Record<Certification, SuperBlocks[]>
|
||||
> = {
|
||||
[Certification.FullStackDeveloperV9]: [
|
||||
SuperBlocks.RespWebDesignV9,
|
||||
SuperBlocks.JsV9,
|
||||
SuperBlocks.FrontEndDevLibsV9,
|
||||
SuperBlocks.PythonV9,
|
||||
SuperBlocks.RelationalDbV9,
|
||||
SuperBlocks.BackEndDevApisV9
|
||||
]
|
||||
};
|
||||
|
||||
export type CertSlug = (typeof Certification)[keyof typeof Certification];
|
||||
|
||||
export const linkedInCredentialIds = {
|
||||
[Certification.LegacyFrontEnd]: 'lfe',
|
||||
[Certification.LegacyBackEnd]: 'lbe',
|
||||
[Certification.LegacyDataVis]: 'ldv',
|
||||
[Certification.LegacyInfoSecQa]: 'lisaqa',
|
||||
[Certification.LegacyFullStack]: 'lfs',
|
||||
[Certification.RespWebDesign]: 'rwd',
|
||||
[Certification.FrontEndDevLibs]: 'fedl',
|
||||
[Certification.JsAlgoDataStruct]: 'ljaads',
|
||||
[Certification.DataVis]: 'dv',
|
||||
[Certification.BackEndDevApis]: 'bedaa',
|
||||
[Certification.QualityAssurance]: 'qa',
|
||||
[Certification.InfoSec]: 'is',
|
||||
[Certification.SciCompPy]: 'scwp',
|
||||
[Certification.DataAnalysisPy]: 'dawp',
|
||||
[Certification.MachineLearningPy]: 'mlwp',
|
||||
[Certification.RelationalDb]: 'rd',
|
||||
[Certification.CollegeAlgebraPy]: 'cawp',
|
||||
[Certification.FoundationalCSharp]: 'fcswm',
|
||||
[Certification.RespWebDesignV9]: 'rwdv9',
|
||||
[Certification.JsV9]: 'jsv9',
|
||||
[Certification.FrontEndDevLibsV9]: 'felv9',
|
||||
[Certification.PythonV9]: 'pyv9',
|
||||
[Certification.RelationalDbV9]: 'rdv9',
|
||||
[Certification.BackEndDevApisV9]: 'bedv9',
|
||||
[Certification.FullStackDeveloperV9]: 'fsdv9',
|
||||
[Certification.JsAlgoDataStructNew]: 'jaads',
|
||||
[Certification.A2English]: 'a2efd',
|
||||
[Certification.B1English]: 'b1efd',
|
||||
[Certification.A2Spanish]: 'a2ps',
|
||||
[Certification.A2Chinese]: 'a2pc',
|
||||
[Certification.A1Chinese]: 'a1pc'
|
||||
};
|
||||
|
||||
export const oldDataVizId = '561add10cb82ac38a17513b3';
|
||||
@@ -0,0 +1,197 @@
|
||||
const html = 0;
|
||||
const js = 1;
|
||||
const backend = 2;
|
||||
const zipline = 3;
|
||||
const frontEndProject = 3;
|
||||
const backEndProject = 4;
|
||||
const jsProject = 5;
|
||||
const modern = 6;
|
||||
const step = 7;
|
||||
const quiz = 8;
|
||||
const invalid = 9;
|
||||
const pythonProject = 10;
|
||||
const video = 11;
|
||||
const codeAllyPractice = 12;
|
||||
const codeAllyCert = 13;
|
||||
const multifileCertProject = 14;
|
||||
const theOdinProject = 15;
|
||||
const colab = 16;
|
||||
const exam = 17;
|
||||
const msTrophy = 18;
|
||||
const multipleChoice = 19;
|
||||
const python = 20;
|
||||
const dialogue = 21;
|
||||
const fillInTheBlank = 22;
|
||||
const multifilePythonCertProject = 23;
|
||||
const generic = 24;
|
||||
const lab = 25;
|
||||
const jsLab = 26;
|
||||
const pyLab = 27;
|
||||
const dailyChallengeJs = 28;
|
||||
const dailyChallengePy = 29;
|
||||
const examDownload = 30;
|
||||
const review = 31;
|
||||
|
||||
export const challengeTypes = {
|
||||
html,
|
||||
js,
|
||||
backend,
|
||||
zipline,
|
||||
frontEndProject,
|
||||
backEndProject,
|
||||
pythonProject,
|
||||
jsProject,
|
||||
modern,
|
||||
step,
|
||||
quiz,
|
||||
invalid,
|
||||
video,
|
||||
codeAllyPractice,
|
||||
codeAllyCert,
|
||||
multifileCertProject,
|
||||
theOdinProject,
|
||||
colab,
|
||||
exam,
|
||||
msTrophy,
|
||||
multipleChoice,
|
||||
python,
|
||||
dialogue,
|
||||
fillInTheBlank,
|
||||
multifilePythonCertProject,
|
||||
generic,
|
||||
lab,
|
||||
jsLab,
|
||||
pyLab,
|
||||
dailyChallengeJs,
|
||||
dailyChallengePy,
|
||||
examDownload,
|
||||
review
|
||||
};
|
||||
|
||||
export const hasNoSolution = (challengeType: number): boolean => {
|
||||
const noSolutions = [
|
||||
backend,
|
||||
zipline,
|
||||
frontEndProject,
|
||||
backEndProject,
|
||||
step,
|
||||
quiz,
|
||||
invalid,
|
||||
pythonProject,
|
||||
video,
|
||||
codeAllyPractice,
|
||||
codeAllyCert,
|
||||
theOdinProject,
|
||||
colab,
|
||||
exam,
|
||||
msTrophy,
|
||||
multipleChoice,
|
||||
dialogue,
|
||||
fillInTheBlank,
|
||||
generic,
|
||||
examDownload,
|
||||
review
|
||||
];
|
||||
|
||||
return noSolutions.includes(challengeType);
|
||||
};
|
||||
|
||||
// determine the component view for each challenge
|
||||
export const viewTypes = {
|
||||
[html]: 'classic',
|
||||
[js]: 'classic',
|
||||
[jsProject]: 'classic',
|
||||
[frontEndProject]: 'frontend',
|
||||
[backEndProject]: 'backend',
|
||||
[pythonProject]: 'frontend',
|
||||
[modern]: 'modern',
|
||||
[step]: 'step',
|
||||
[quiz]: 'quiz',
|
||||
[backend]: 'backend',
|
||||
[video]: 'generic',
|
||||
[codeAllyPractice]: 'codeAlly',
|
||||
[codeAllyCert]: 'codeAlly',
|
||||
[multifileCertProject]: 'classic',
|
||||
[theOdinProject]: 'generic',
|
||||
[colab]: 'frontend',
|
||||
[exam]: 'exam',
|
||||
[msTrophy]: 'msTrophy',
|
||||
[multipleChoice]: 'generic',
|
||||
[python]: 'modern',
|
||||
[dialogue]: 'generic',
|
||||
[fillInTheBlank]: 'fillInTheBlank',
|
||||
[multifilePythonCertProject]: 'classic',
|
||||
[generic]: 'generic',
|
||||
[lab]: 'classic',
|
||||
[jsLab]: 'classic',
|
||||
[pyLab]: 'classic',
|
||||
[dailyChallengeJs]: 'classic',
|
||||
[dailyChallengePy]: 'classic',
|
||||
[examDownload]: 'examDownload',
|
||||
[review]: 'generic'
|
||||
};
|
||||
|
||||
// determine the type of submit function to use for the challenge on completion
|
||||
export const submitTypes = {
|
||||
[html]: 'tests',
|
||||
[js]: 'tests',
|
||||
[jsProject]: 'tests',
|
||||
// requires just a single url
|
||||
// like codepen.com/my-project
|
||||
[frontEndProject]: 'project.frontEnd',
|
||||
// requires two urls
|
||||
// a hosted URL where the app is running live
|
||||
// project code url like GitHub
|
||||
[backEndProject]: 'project.backEnd',
|
||||
[pythonProject]: 'project.backEnd',
|
||||
[step]: 'step',
|
||||
[quiz]: 'tests',
|
||||
[backend]: 'backend',
|
||||
[modern]: 'tests',
|
||||
[video]: 'tests',
|
||||
[codeAllyCert]: 'project.frontEnd',
|
||||
[multifileCertProject]: 'tests',
|
||||
[theOdinProject]: 'tests',
|
||||
[colab]: 'project.backEnd',
|
||||
[exam]: 'exam',
|
||||
[msTrophy]: 'msTrophy',
|
||||
[multipleChoice]: 'tests',
|
||||
[python]: 'tests',
|
||||
[dialogue]: 'tests',
|
||||
[fillInTheBlank]: 'tests',
|
||||
[multifilePythonCertProject]: 'tests',
|
||||
[generic]: 'tests',
|
||||
[lab]: 'tests',
|
||||
[jsLab]: 'tests',
|
||||
[pyLab]: 'tests',
|
||||
[dailyChallengeJs]: 'tests',
|
||||
[dailyChallengePy]: 'tests',
|
||||
[examDownload]: 'examDownload',
|
||||
[review]: 'tests'
|
||||
};
|
||||
|
||||
const dailyCodingChallengeTypes = [
|
||||
challengeTypes.dailyChallengeJs,
|
||||
challengeTypes.dailyChallengePy
|
||||
];
|
||||
|
||||
export const getIsDailyCodingChallenge = (challengeType: number): boolean =>
|
||||
dailyCodingChallengeTypes.includes(challengeType);
|
||||
|
||||
const labChallengeTypes = [
|
||||
challengeTypes.lab,
|
||||
challengeTypes.jsLab,
|
||||
challengeTypes.pyLab
|
||||
];
|
||||
|
||||
export const getIsLabChallenge = (challengeType: number): boolean =>
|
||||
labChallengeTypes.includes(challengeType);
|
||||
|
||||
const dailyCodingChallengeLanguages = {
|
||||
[challengeTypes.dailyChallengeJs]: 'javascript',
|
||||
[challengeTypes.dailyChallengePy]: 'python'
|
||||
};
|
||||
|
||||
export const getDailyCodingChallengeLanguage = (
|
||||
challengeType: number
|
||||
): string | undefined => dailyCodingChallengeLanguages[challengeType];
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Module } from './modules';
|
||||
|
||||
// TODO: Dynamically create these from intro.json or full-stack.json
|
||||
export enum FsdChapters {
|
||||
// original FSD
|
||||
Welcome = 'freecodecamp',
|
||||
Javascript = 'javascript',
|
||||
FrontendLibraries = 'frontend-libraries',
|
||||
BackendJavascript = 'backend-javascript',
|
||||
Career = 'career',
|
||||
|
||||
// new FSD
|
||||
RwdExam = 'responsive-web-design-certification-exam',
|
||||
JsExam = 'javascript-certification-exam',
|
||||
Fed = 'front-end-development-libraries',
|
||||
FedExam = 'front-end-development-libraries-certification-exam',
|
||||
PythonExam = 'python-certification-exam',
|
||||
RdbExam = 'relational-databases-certification-exam',
|
||||
Bed = 'back-end-development-and-apis',
|
||||
BedExam = 'back-end-development-and-apis-certification-exam',
|
||||
FsdExam = 'certified-full-stack-developer-exam',
|
||||
|
||||
// used in both
|
||||
Html = 'html',
|
||||
Css = 'css',
|
||||
Python = 'python',
|
||||
RelationalDatabases = 'relational-databases'
|
||||
}
|
||||
|
||||
export enum A1ChineseChapters {
|
||||
zhA1Welcome = 'zh-a1-chapter-welcome-to-a1-professional-chinese',
|
||||
zhA1PinYin = 'zh-a1-chapter-pinyin',
|
||||
zhA1Greetings = 'zh-a1-chapter-greetings-and-introductions',
|
||||
zhA1GreetingsLegacy = 'zh-a1-chapter-greeting-and-self-introduction',
|
||||
zhA1NumbersAndPersonalInformation = 'zh-a1-chapter-numbers-and-personal-information',
|
||||
zhA1Family = 'zh-a1-chapter-introducing-colleagues-and-family',
|
||||
zhA1Expressing = 'zh-a1-chapter-expressing-what-you-can-and-cant-do'
|
||||
}
|
||||
|
||||
export enum A1SpanishChapters {
|
||||
esA1Welcome = 'es-a1-chapter-welcome-to-a1-professional-spanish',
|
||||
esA1Fundamentals = 'es-a1-chapter-spanish-fundamentals',
|
||||
esA1Greetings = 'es-a1-chapter-greetings-and-introductions',
|
||||
esA1Details = 'es-a1-chapter-basic-personal-details',
|
||||
esA1Describing = 'es-a1-chapter-describing-company-and-people'
|
||||
}
|
||||
|
||||
export interface Chapter {
|
||||
dashedName: string;
|
||||
comingSoon?: boolean;
|
||||
modules: Module[];
|
||||
chapterType?: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { blocklistedUsernames } from './constants';
|
||||
|
||||
describe('constants', () => {
|
||||
describe('blocklistedUsernames', () => {
|
||||
it('should not contain duplicate values', () => {
|
||||
const uniqueValues = new Set(blocklistedUsernames);
|
||||
expect(blocklistedUsernames.length).toEqual(uniqueValues.size);
|
||||
});
|
||||
|
||||
it('should contain all the letters in the latin alphabet', () => {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
|
||||
expect(blocklistedUsernames).toEqual(expect.arrayContaining(alphabet));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Type tests:
|
||||
type BlocklistedUsernames = (typeof blocklistedUsernames)[number];
|
||||
|
||||
type HasString = string extends BlocklistedUsernames ? true : false;
|
||||
|
||||
type Expect<T extends true> = T;
|
||||
|
||||
// @ts-expect-error - This is intended to fail since we want to ensure that blocklistedUsernames is an array of literals
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Test = Expect<HasString>;
|
||||
@@ -0,0 +1,656 @@
|
||||
const alphabet = [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z'
|
||||
] as const;
|
||||
|
||||
export const i18nConstants = [
|
||||
// reserved paths for localizations
|
||||
'afrikaans',
|
||||
'arabic',
|
||||
'bengali',
|
||||
'catalan',
|
||||
'chinese',
|
||||
'czech',
|
||||
'danish',
|
||||
'dutch',
|
||||
'espanol',
|
||||
'finnish',
|
||||
'french',
|
||||
'german',
|
||||
'greek',
|
||||
'hebrew',
|
||||
'hindi',
|
||||
'hungarian',
|
||||
'italian',
|
||||
'japanese',
|
||||
'korean',
|
||||
'norwegian',
|
||||
'polish',
|
||||
'portuguese',
|
||||
'romanian',
|
||||
'russian',
|
||||
'serbian',
|
||||
'spanish',
|
||||
'swahili',
|
||||
'swedish',
|
||||
'turkish',
|
||||
'ukrainian',
|
||||
'vietnamese'
|
||||
] as const;
|
||||
|
||||
export const blocklistedUsernames = [
|
||||
...alphabet,
|
||||
...i18nConstants,
|
||||
'about',
|
||||
'academic-honesty',
|
||||
'account',
|
||||
'agile',
|
||||
'all-stories',
|
||||
'api',
|
||||
'backend-challenge-completed',
|
||||
'blocked',
|
||||
'bonfire',
|
||||
'catalog',
|
||||
'cats.json',
|
||||
'challenge-completed',
|
||||
'challenge',
|
||||
'challenges',
|
||||
'chat',
|
||||
'code-of-conduct',
|
||||
'coding-bootcamp-cost-calculator',
|
||||
'completed-bonfire',
|
||||
'completed-challenge',
|
||||
'completed-field-guide',
|
||||
'completed-jsProject',
|
||||
'completed-zipline-or-basejump',
|
||||
'copyright-policy',
|
||||
'copyright',
|
||||
'deprecated-signin',
|
||||
'donate',
|
||||
'email-signin',
|
||||
'events',
|
||||
'exam',
|
||||
'explorer',
|
||||
'external',
|
||||
'field-guide',
|
||||
'forgot',
|
||||
'forum',
|
||||
'freecodecamp',
|
||||
'get-help',
|
||||
'get-pai',
|
||||
'guide',
|
||||
'how-nonprofit-projects-work',
|
||||
'internal',
|
||||
'jobs-form',
|
||||
'jobs',
|
||||
'jsProject',
|
||||
'learn-to-code',
|
||||
'learn',
|
||||
'login',
|
||||
'logout',
|
||||
'map',
|
||||
'modern-challenge-completed',
|
||||
'news',
|
||||
'nonprofit-project-instructions',
|
||||
'nonprofits-form',
|
||||
'nonprofits',
|
||||
'open-api',
|
||||
'passwordless-change',
|
||||
'pmi-acp-agile-project-managers-form',
|
||||
'pmi-acp-agile-project-managers',
|
||||
'privacy-policy',
|
||||
'privacy',
|
||||
'profile',
|
||||
'project-completed',
|
||||
'reset',
|
||||
'services',
|
||||
'shop',
|
||||
'signin',
|
||||
'signout',
|
||||
'signup',
|
||||
'sitemap.xml',
|
||||
'software-resources-for-nonprofits',
|
||||
'sponsors',
|
||||
'stories',
|
||||
'support',
|
||||
'terms-of-service',
|
||||
'terms',
|
||||
'the-fastest-web-page-on-the-internet',
|
||||
'twitch',
|
||||
'unsubscribe',
|
||||
'unsubscribed',
|
||||
'update-my-honesty',
|
||||
'update-my-portfolio',
|
||||
'update-my-profile-ui',
|
||||
'update-my-quincy-email',
|
||||
'update-my-socials',
|
||||
'update-my-sound',
|
||||
'update-my-theme',
|
||||
'update-my-keyboard-shortcuts',
|
||||
'update-my-username',
|
||||
'user',
|
||||
'username',
|
||||
'wiki',
|
||||
// some more names from https://github.com/marteinn/The-Big-Username-Blacklist-JS/blob/master/src/list.js
|
||||
'.htaccess',
|
||||
'.htpasswd',
|
||||
'.well-known',
|
||||
'400',
|
||||
'401',
|
||||
'403',
|
||||
'404',
|
||||
'405',
|
||||
'406',
|
||||
'407',
|
||||
'408',
|
||||
'409',
|
||||
'410',
|
||||
'411',
|
||||
'412',
|
||||
'413',
|
||||
'414',
|
||||
'415',
|
||||
'416',
|
||||
'417',
|
||||
'421',
|
||||
'422',
|
||||
'423',
|
||||
'424',
|
||||
'426',
|
||||
'428',
|
||||
'429',
|
||||
'431',
|
||||
'500',
|
||||
'501',
|
||||
'502',
|
||||
'503',
|
||||
'504',
|
||||
'505',
|
||||
'506',
|
||||
'507',
|
||||
'508',
|
||||
'509',
|
||||
'510',
|
||||
'511',
|
||||
'about-us',
|
||||
'abuse',
|
||||
'access',
|
||||
'accounts',
|
||||
'ad',
|
||||
'add',
|
||||
'admin',
|
||||
'administration',
|
||||
'administrator',
|
||||
'ads',
|
||||
'advertise',
|
||||
'advertising',
|
||||
'aes128-ctr',
|
||||
'aes128-gcm',
|
||||
'aes192-ctr',
|
||||
'aes256-ctr',
|
||||
'aes256-gcm',
|
||||
'affiliate',
|
||||
'affiliates',
|
||||
'ajax',
|
||||
'alert',
|
||||
'alerts',
|
||||
'alpha',
|
||||
'amp',
|
||||
'analytics',
|
||||
'app',
|
||||
'apps',
|
||||
'asc',
|
||||
'assets',
|
||||
'atom',
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorize',
|
||||
'autoconfig',
|
||||
'autodiscover',
|
||||
'avatar',
|
||||
'backup',
|
||||
'banner',
|
||||
'banners',
|
||||
'beta',
|
||||
'billing',
|
||||
'billings',
|
||||
'blog',
|
||||
'blogs',
|
||||
'board',
|
||||
'bookmark',
|
||||
'bookmarks',
|
||||
'broadcasthost',
|
||||
'business',
|
||||
'buy',
|
||||
'cache',
|
||||
'calendar',
|
||||
'campaign',
|
||||
'captcha',
|
||||
'careers',
|
||||
'cart',
|
||||
'cas',
|
||||
'categories',
|
||||
'category',
|
||||
'cdn',
|
||||
'cgi',
|
||||
'cgi-bin',
|
||||
'chacha20-poly1305',
|
||||
'change',
|
||||
'channel',
|
||||
'channels',
|
||||
'chart',
|
||||
'checkout',
|
||||
'clear',
|
||||
'client',
|
||||
'close',
|
||||
'cms',
|
||||
'com',
|
||||
'comment',
|
||||
'comments',
|
||||
'community',
|
||||
'compare',
|
||||
'compose',
|
||||
'config',
|
||||
'connect',
|
||||
'contact',
|
||||
'contest',
|
||||
'cookies',
|
||||
'copy',
|
||||
'count',
|
||||
'create',
|
||||
'crossdomain.xml',
|
||||
'css',
|
||||
'curve25519-sha256',
|
||||
'customer',
|
||||
'customers',
|
||||
'customize',
|
||||
'dashboard',
|
||||
'db',
|
||||
'deals',
|
||||
'debug',
|
||||
'delete',
|
||||
'desc',
|
||||
'destroy',
|
||||
'dev',
|
||||
'developer',
|
||||
'developers',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'disconnect',
|
||||
'discuss',
|
||||
'dns',
|
||||
'dns0',
|
||||
'dns1',
|
||||
'dns2',
|
||||
'dns3',
|
||||
'dns4',
|
||||
'docs',
|
||||
'documentation',
|
||||
'domain',
|
||||
'download',
|
||||
'downloads',
|
||||
'downvote',
|
||||
'draft',
|
||||
'drop',
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'edit',
|
||||
'editor',
|
||||
'email',
|
||||
'enterprise',
|
||||
'error',
|
||||
'errors',
|
||||
'event',
|
||||
'example',
|
||||
'exception',
|
||||
'exit',
|
||||
'explore',
|
||||
'export',
|
||||
'extensions',
|
||||
'false',
|
||||
'family',
|
||||
'faq',
|
||||
'faqs',
|
||||
'favicon.ico',
|
||||
'features',
|
||||
'feed',
|
||||
'feedback',
|
||||
'feeds',
|
||||
'file',
|
||||
'files',
|
||||
'filter',
|
||||
'follow',
|
||||
'follower',
|
||||
'followers',
|
||||
'following',
|
||||
'fonts',
|
||||
'forgot-password',
|
||||
'forgotpassword',
|
||||
'form',
|
||||
'forms',
|
||||
'forums',
|
||||
'friend',
|
||||
'friends',
|
||||
'ftp',
|
||||
'get',
|
||||
'git',
|
||||
'go',
|
||||
'group',
|
||||
'groups',
|
||||
'guest',
|
||||
'guidelines',
|
||||
'guides',
|
||||
'head',
|
||||
'header',
|
||||
'help',
|
||||
'hide',
|
||||
'hmac-sha',
|
||||
'hmac-sha1',
|
||||
'hmac-sha1-etm',
|
||||
'hmac-sha2-256',
|
||||
'hmac-sha2-256-etm',
|
||||
'hmac-sha2-512',
|
||||
'hmac-sha2-512-etm',
|
||||
'home',
|
||||
'host',
|
||||
'hosting',
|
||||
'hostmaster',
|
||||
'htpasswd',
|
||||
'http',
|
||||
'httpd',
|
||||
'https',
|
||||
'humans.txt',
|
||||
'icons',
|
||||
'images',
|
||||
'imap',
|
||||
'img',
|
||||
'import',
|
||||
'index',
|
||||
'info',
|
||||
'insert',
|
||||
'investors',
|
||||
'invitations',
|
||||
'invite',
|
||||
'invites',
|
||||
'invoice',
|
||||
'is',
|
||||
'isatap',
|
||||
'issues',
|
||||
'it',
|
||||
'join',
|
||||
'js',
|
||||
'json',
|
||||
'keybase.txt',
|
||||
'legal',
|
||||
'license',
|
||||
'licensing',
|
||||
'like',
|
||||
'limit',
|
||||
'live',
|
||||
'load',
|
||||
'local',
|
||||
'localdomain',
|
||||
'localhost',
|
||||
'lock',
|
||||
'lost-password',
|
||||
'mail',
|
||||
'mail0',
|
||||
'mail1',
|
||||
'mail2',
|
||||
'mail3',
|
||||
'mail4',
|
||||
'mail5',
|
||||
'mail6',
|
||||
'mail7',
|
||||
'mail8',
|
||||
'mail9',
|
||||
'mailer-daemon',
|
||||
'mailerdaemon',
|
||||
'marketing',
|
||||
'marketplace',
|
||||
'master',
|
||||
'me',
|
||||
'media',
|
||||
'member',
|
||||
'members',
|
||||
'message',
|
||||
'messages',
|
||||
'metrics',
|
||||
'mis',
|
||||
'mobile',
|
||||
'moderator',
|
||||
'modify',
|
||||
'more',
|
||||
'mx',
|
||||
'my',
|
||||
'net',
|
||||
'network',
|
||||
'new',
|
||||
'newsletter',
|
||||
'newsletters',
|
||||
'next',
|
||||
'nil',
|
||||
'no-reply',
|
||||
'nobody',
|
||||
'noc',
|
||||
'none',
|
||||
'noreply',
|
||||
'notification',
|
||||
'notifications',
|
||||
'ns',
|
||||
'ns0',
|
||||
'ns1',
|
||||
'ns2',
|
||||
'ns3',
|
||||
'ns4',
|
||||
'ns5',
|
||||
'ns6',
|
||||
'ns7',
|
||||
'ns8',
|
||||
'ns9',
|
||||
'null',
|
||||
'oauth',
|
||||
'oauth2',
|
||||
'offer',
|
||||
'offers',
|
||||
'online',
|
||||
'openid',
|
||||
'order',
|
||||
'orders',
|
||||
'overview',
|
||||
'owner',
|
||||
'page',
|
||||
'pages',
|
||||
'partners',
|
||||
'passwd',
|
||||
'password',
|
||||
'pay',
|
||||
'payment',
|
||||
'payments',
|
||||
'photo',
|
||||
'photos',
|
||||
'pixel',
|
||||
'plans',
|
||||
'plugins',
|
||||
'policies',
|
||||
'policy',
|
||||
'pop',
|
||||
'pop3',
|
||||
'popular',
|
||||
'portfolio',
|
||||
'post',
|
||||
'postfix',
|
||||
'postmaster',
|
||||
'poweruser',
|
||||
'preferences',
|
||||
'premium',
|
||||
'press',
|
||||
'previous',
|
||||
'pricing',
|
||||
'print',
|
||||
'private',
|
||||
'prod',
|
||||
'product',
|
||||
'production',
|
||||
'profiles',
|
||||
'project',
|
||||
'projects',
|
||||
'public',
|
||||
'purchase',
|
||||
'put',
|
||||
'quota',
|
||||
'redirect',
|
||||
'reduce',
|
||||
'refund',
|
||||
'refunds',
|
||||
'register',
|
||||
'registration',
|
||||
'remove',
|
||||
'replies',
|
||||
'reply',
|
||||
'report',
|
||||
'request',
|
||||
'request-password',
|
||||
'reset-password',
|
||||
'response',
|
||||
'return',
|
||||
'returns',
|
||||
'review',
|
||||
'reviews',
|
||||
'robots.txt',
|
||||
'root',
|
||||
'rootuser',
|
||||
'rsa-sha2-2',
|
||||
'rsa-sha2-512',
|
||||
'rss',
|
||||
'rules',
|
||||
'sales',
|
||||
'save',
|
||||
'script',
|
||||
'sdk',
|
||||
'search',
|
||||
'secure',
|
||||
'security',
|
||||
'select',
|
||||
'session',
|
||||
'sessions',
|
||||
'settings',
|
||||
'setup',
|
||||
'share',
|
||||
'shift',
|
||||
'site',
|
||||
'sitemap',
|
||||
'sites',
|
||||
'smtp',
|
||||
'sort',
|
||||
'source',
|
||||
'sql',
|
||||
'ssh',
|
||||
'ssh-rsa',
|
||||
'ssl',
|
||||
'ssladmin',
|
||||
'ssladministrator',
|
||||
'sslwebmaster',
|
||||
'stage',
|
||||
'staging',
|
||||
'stat',
|
||||
'static',
|
||||
'statistics',
|
||||
'stats',
|
||||
'status',
|
||||
'store',
|
||||
'style',
|
||||
'styles',
|
||||
'stylesheet',
|
||||
'stylesheets',
|
||||
'subdomain',
|
||||
'subscribe',
|
||||
'sudo',
|
||||
'super',
|
||||
'superuser',
|
||||
'survey',
|
||||
'sync',
|
||||
'sysadmin',
|
||||
'system',
|
||||
'tablet',
|
||||
'tag',
|
||||
'tags',
|
||||
'team',
|
||||
'telnet',
|
||||
'terms-of-use',
|
||||
'test',
|
||||
'testimonials',
|
||||
'theme',
|
||||
'themes',
|
||||
'today',
|
||||
'tools',
|
||||
'topic',
|
||||
'topics',
|
||||
'tour',
|
||||
'training',
|
||||
'translate',
|
||||
'translations',
|
||||
'trending',
|
||||
'trial',
|
||||
'true',
|
||||
'umac-128',
|
||||
'umac-128-etm',
|
||||
'umac-64',
|
||||
'umac-64-etm',
|
||||
'undefined',
|
||||
'unfollow',
|
||||
'unlike',
|
||||
'update',
|
||||
'upgrade',
|
||||
'usenet',
|
||||
'users',
|
||||
'uucp',
|
||||
'var',
|
||||
'verify',
|
||||
'video',
|
||||
'view',
|
||||
'void',
|
||||
'vote',
|
||||
'webmail',
|
||||
'webmaster',
|
||||
'website',
|
||||
'widget',
|
||||
'widgets',
|
||||
'wpad',
|
||||
'write',
|
||||
'www',
|
||||
'www-data',
|
||||
'www1',
|
||||
'www2',
|
||||
'www3',
|
||||
'www4',
|
||||
'you',
|
||||
'yourname',
|
||||
'yourusername',
|
||||
'zlib'
|
||||
] as const;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Languages } from './i18n';
|
||||
import {
|
||||
SuperBlocks,
|
||||
SuperBlockStage,
|
||||
superBlockStages,
|
||||
notAuditedSuperBlocks,
|
||||
generateSuperBlockList,
|
||||
getAuditedSuperBlocks
|
||||
} from './curriculum';
|
||||
|
||||
describe('superBlockOrder', () => {
|
||||
it('should contain all SuperBlocks', () => {
|
||||
const allSuperBlocks = Object.values(SuperBlocks);
|
||||
const superBlockOrderValues = Object.values(superBlockStages).flat();
|
||||
expect(superBlockOrderValues).toHaveLength(allSuperBlocks.length);
|
||||
expect(superBlockOrderValues).toEqual(
|
||||
expect.arrayContaining(allSuperBlocks)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSuperBlockList', () => {
|
||||
it('should return an array of SuperBlocks object with all elements when if all configs are true', () => {
|
||||
const result = generateSuperBlockList({
|
||||
showUpcomingChanges: true
|
||||
});
|
||||
expect(result).toHaveLength(Object.values(superBlockStages).flat().length);
|
||||
});
|
||||
|
||||
it('should return an array of SuperBlocks without Upcoming when { showUpcomingChanges: false }', () => {
|
||||
const result = generateSuperBlockList({
|
||||
showUpcomingChanges: false
|
||||
});
|
||||
const tempSuperBlockMap = { ...superBlockStages };
|
||||
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
|
||||
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Immutability of superBlockOrder, notAuditedSuperBlocks, and flatSuperBlockMap', () => {
|
||||
it('should not allow modification of superBlockOrder', () => {
|
||||
expect(() => {
|
||||
superBlockStages[SuperBlockStage.Core] = [];
|
||||
}).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it('should not allow modification of notAuditedSuperBlocks', () => {
|
||||
expect(() => {
|
||||
notAuditedSuperBlocks[Languages.English] = [];
|
||||
}).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it('should not allow modification of flatSuperBlockMap', () => {
|
||||
expect(() => {
|
||||
notAuditedSuperBlocks[Languages.English] = [];
|
||||
}).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuditedSuperBlocks', () => {
|
||||
Object.keys(notAuditedSuperBlocks).forEach(language => {
|
||||
it(`should return only audited SuperBlocks for ${language}`, () => {
|
||||
const auditedSuperBlocks = getAuditedSuperBlocks({
|
||||
language
|
||||
});
|
||||
|
||||
auditedSuperBlocks.forEach(superblock => {
|
||||
expect(notAuditedSuperBlocks[language as Languages]).not.toContain(
|
||||
superblock
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
// TODO: eventually this should all flow from the curriculum service, since it
|
||||
// defines the top-level structure of the curriculum.
|
||||
import { Languages } from './i18n.js';
|
||||
|
||||
// all superblocks
|
||||
export enum SuperBlocks {
|
||||
RespWebDesignNew = '2022/responsive-web-design',
|
||||
RespWebDesign = 'responsive-web-design',
|
||||
JsAlgoDataStruct = 'javascript-algorithms-and-data-structures',
|
||||
JsAlgoDataStructNew = 'javascript-algorithms-and-data-structures-v8',
|
||||
FrontEndDevLibs = 'front-end-development-libraries',
|
||||
DataVis = 'data-visualization',
|
||||
RelationalDb = 'relational-database',
|
||||
BackEndDevApis = 'back-end-development-and-apis',
|
||||
QualityAssurance = 'quality-assurance',
|
||||
SciCompPy = 'scientific-computing-with-python',
|
||||
DataAnalysisPy = 'data-analysis-with-python',
|
||||
InfoSec = 'information-security',
|
||||
MachineLearningPy = 'machine-learning-with-python',
|
||||
CodingInterviewPrep = 'coding-interview-prep',
|
||||
TheOdinProject = 'the-odin-project',
|
||||
ProjectEuler = 'project-euler',
|
||||
CollegeAlgebraPy = 'college-algebra-with-python',
|
||||
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
|
||||
A2English = 'a2-english-for-developers',
|
||||
B1English = 'b1-english-for-developers',
|
||||
A1Spanish = 'a1-professional-spanish',
|
||||
A2Spanish = 'a2-professional-spanish',
|
||||
A2Chinese = 'a2-professional-chinese',
|
||||
A1Chinese = 'a1-professional-chinese',
|
||||
RosettaCode = 'rosetta-code',
|
||||
PythonForEverybody = 'python-for-everybody',
|
||||
BasicHtml = 'basic-html',
|
||||
SemanticHtml = 'semantic-html',
|
||||
DevPlayground = 'dev-playground',
|
||||
FullStackOpen = 'full-stack-open',
|
||||
RespWebDesignV9 = 'responsive-web-design-v9',
|
||||
JsV9 = 'javascript-v9',
|
||||
FrontEndDevLibsV9 = 'front-end-development-libraries-v9',
|
||||
PythonV9 = 'python-v9',
|
||||
RelationalDbV9 = 'relational-databases-v9',
|
||||
BackEndDevApisV9 = 'back-end-development-and-apis-v9',
|
||||
FullStackDeveloperV9 = 'full-stack-developer-v9',
|
||||
HtmlFormsAndTables = 'html-forms-and-tables',
|
||||
HtmlAndAccessibility = 'html-and-accessibility',
|
||||
ComputerBasics = 'computer-basics',
|
||||
BasicCss = 'basic-css',
|
||||
DesignForDevelopers = 'design-for-developers',
|
||||
AbsoluteAndRelativeUnits = 'absolute-and-relative-units',
|
||||
PseudoClassesAndElements = 'pseudo-classes-and-elements',
|
||||
CssColors = 'css-colors',
|
||||
StylingForms = 'styling-forms',
|
||||
CssBoxModel = 'css-box-model',
|
||||
CssFlexbox = 'css-flexbox',
|
||||
CssTypography = 'css-typography',
|
||||
CssAndAccessibility = 'css-and-accessibility',
|
||||
CssPositioning = 'css-positioning',
|
||||
AttributeSelectors = 'attribute-selectors',
|
||||
ResponsiveDesign = 'responsive-design',
|
||||
CssVariables = 'css-variables',
|
||||
CssGrid = 'css-grid',
|
||||
CssAnimations = 'css-animations',
|
||||
LearnPythonForBeginners = 'learn-python-for-beginners',
|
||||
IntroductionToAlgorithmsAndDataStructures = 'introduction-to-algorithms-and-data-structures',
|
||||
LearnOOPWithPython = 'learn-oop-with-python',
|
||||
LearnRAGAndMCPFundamentals = 'learn-rag-mcp-fundamentals',
|
||||
IntroductionToPrecalculus = 'introduction-to-precalculus',
|
||||
IntroductionToBash = 'introduction-to-bash',
|
||||
IntroductionToSQLAndPostgreSQL = 'introduction-to-sql-and-postgresql',
|
||||
LearnBashScripting = 'learn-bash-scripting',
|
||||
LearnSQLAndBash = 'learn-sql-and-bash',
|
||||
IntroductionToNano = 'introduction-to-nano',
|
||||
IntroductionToGitAndGithub = 'introduction-to-git-and-github',
|
||||
IntroductionToVariablesAndStringsInJS = 'introduction-to-variables-and-strings-in-javascript',
|
||||
IntroductionToBooleansAndNumbersInJS = 'introduction-to-booleans-and-numbers-in-javascript',
|
||||
IntroductionToFunctionsInJS = 'introduction-functions-in-javascript',
|
||||
IntroductionToArraysInJS = 'introduction-to-arrays-in-javascript',
|
||||
IntroductionToObjectsInJS = 'introduction-to-objects-in-javascript',
|
||||
IntroductionToLoopsInJS = 'introduction-to-loops-in-javascript',
|
||||
JavascriptFundamentalsReview = 'javascript-fundamentals-review',
|
||||
IntroductionToHigherOrderFunctionsAndCallbacksInJS = 'introduction-to-higher-order-functions-and-callbacks-in-javascript',
|
||||
LearnDomManipulationAndEventsWithJS = 'learn-dom-manipulation-and-events-with-javascript',
|
||||
IntroductionToJavascriptAndAccessibility = 'introduction-to-javascript-and-accessibility',
|
||||
LearnJavascriptDebugging = 'learn-javascript-debugging',
|
||||
LearnBasicRegexWithJS = 'learn-basic-regex-with-javascript',
|
||||
IntroductionToDatesInJS = 'introduction-to-dates-in-javascript',
|
||||
LearnAudioAndVideoEventsWithJS = 'learn-audio-and-video-events-with-javascript',
|
||||
IntroductionToMapsAndSetsInJS = 'introduction-to-maps-and-sets-in-javascript',
|
||||
LearnLocalstorageAndCrudOperationsWithJS = 'learn-localstorage-and-crud-operations-with-javascript',
|
||||
IntroductionToJavascriptClasses = 'introduction-to-javascript-classes',
|
||||
LearnRecursionWithJS = 'learn-recursion-with-javascript',
|
||||
IntroductionToFunctionalProgrammingWithJS = 'introduction-to-functional-programming-with-javascript',
|
||||
IntroductionToAsynchronousJS = 'introduction-to-asynchronous-javascript',
|
||||
LearnDataVisualizationWithD3 = 'learn-data-visualization-with-d3',
|
||||
IntroductionToPythonBasics = 'introduction-to-python-basics',
|
||||
LearnPythonLoopsAndSequences = 'learn-python-loops-and-sequences',
|
||||
LearnPythonDictionariesAndSets = 'learn-python-dictionaries-and-sets',
|
||||
LearnErrorHandlingInPython = 'learn-error-handling-in-python',
|
||||
LearnPythonClassesAndObjects = 'learn-python-classes-and-objects',
|
||||
IntroductionToOOPInPython = 'introduction-to-oop-in-python',
|
||||
IntroductionToLinearDataStructuresInPython = 'introduction-to-linear-data-structures-in-python',
|
||||
LearnAlgorithmsInPython = 'learn-algorithms-in-python',
|
||||
LearnGraphsAndTreesInPython = 'learn-graphs-and-trees-in-python',
|
||||
LearnDynamicProgrammingInPython = 'learn-dynamic-programming-in-python'
|
||||
}
|
||||
|
||||
export const languageSuperBlocks = [
|
||||
SuperBlocks.A2English,
|
||||
SuperBlocks.B1English,
|
||||
SuperBlocks.A1Spanish,
|
||||
SuperBlocks.A2Spanish,
|
||||
SuperBlocks.A1Chinese,
|
||||
SuperBlocks.A2Chinese
|
||||
];
|
||||
|
||||
export enum ChallengeLang {
|
||||
English = 'en-US',
|
||||
Spanish = 'es',
|
||||
Chinese = 'zh-CN'
|
||||
}
|
||||
|
||||
// Mapping from superblock to a speech recognition language (BCP-47)
|
||||
export const superBlockToSpeechLang: Partial<
|
||||
Record<SuperBlocks, ChallengeLang>
|
||||
> = {
|
||||
[SuperBlocks.A1Chinese]: ChallengeLang.Chinese,
|
||||
[SuperBlocks.A2Chinese]: ChallengeLang.Chinese,
|
||||
[SuperBlocks.A2English]: ChallengeLang.English,
|
||||
[SuperBlocks.B1English]: ChallengeLang.English,
|
||||
[SuperBlocks.A1Spanish]: ChallengeLang.Spanish,
|
||||
[SuperBlocks.A2Spanish]: ChallengeLang.Spanish
|
||||
};
|
||||
|
||||
/*
|
||||
* SuperBlockStages.Upcoming = SHOW_UPCOMING_CHANGES === 'true'
|
||||
* 'Upcoming' is for development -> not shown on stag or prod anywhere
|
||||
*
|
||||
* SuperBlockStages.Next = deployed, but only shown if the Growthbook feature
|
||||
* is enabled.
|
||||
*
|
||||
*/
|
||||
export enum SuperBlockStage {
|
||||
Core,
|
||||
English,
|
||||
Spanish,
|
||||
Chinese,
|
||||
Professional,
|
||||
Extra,
|
||||
Legacy,
|
||||
Upcoming,
|
||||
Next,
|
||||
Catalog
|
||||
}
|
||||
|
||||
const defaultStageOrder = [
|
||||
SuperBlockStage.Core,
|
||||
SuperBlockStage.English,
|
||||
SuperBlockStage.Spanish,
|
||||
SuperBlockStage.Chinese,
|
||||
SuperBlockStage.Extra,
|
||||
SuperBlockStage.Legacy,
|
||||
SuperBlockStage.Professional,
|
||||
SuperBlockStage.Next,
|
||||
SuperBlockStage.Catalog
|
||||
];
|
||||
|
||||
export function getStageOrder({
|
||||
showUpcomingChanges
|
||||
}: Config): SuperBlockStage[] {
|
||||
const stageOrder = [...defaultStageOrder];
|
||||
|
||||
if (showUpcomingChanges) {
|
||||
stageOrder.push(SuperBlockStage.Upcoming);
|
||||
}
|
||||
return stageOrder;
|
||||
}
|
||||
|
||||
export type StageMap = {
|
||||
[key in SuperBlockStage]: SuperBlocks[];
|
||||
};
|
||||
|
||||
// Groups of superblocks in learn map. This should include all superblocks.
|
||||
export const superBlockStages: StageMap = {
|
||||
[SuperBlockStage.Core]: [
|
||||
SuperBlocks.RespWebDesignV9,
|
||||
SuperBlocks.JsV9,
|
||||
SuperBlocks.FrontEndDevLibsV9,
|
||||
SuperBlocks.PythonV9,
|
||||
SuperBlocks.RelationalDbV9,
|
||||
SuperBlocks.BackEndDevApisV9,
|
||||
SuperBlocks.FullStackDeveloperV9
|
||||
],
|
||||
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
|
||||
[SuperBlockStage.Spanish]: [SuperBlocks.A1Spanish],
|
||||
[SuperBlockStage.Chinese]: [SuperBlocks.A1Chinese],
|
||||
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
|
||||
[SuperBlockStage.Extra]: [
|
||||
SuperBlocks.TheOdinProject,
|
||||
SuperBlocks.CodingInterviewPrep,
|
||||
SuperBlocks.ProjectEuler,
|
||||
SuperBlocks.RosettaCode
|
||||
],
|
||||
[SuperBlockStage.Legacy]: [
|
||||
SuperBlocks.RespWebDesignNew,
|
||||
SuperBlocks.JsAlgoDataStructNew,
|
||||
SuperBlocks.FrontEndDevLibs,
|
||||
SuperBlocks.DataVis,
|
||||
SuperBlocks.RelationalDb,
|
||||
SuperBlocks.BackEndDevApis,
|
||||
SuperBlocks.QualityAssurance,
|
||||
SuperBlocks.SciCompPy,
|
||||
SuperBlocks.DataAnalysisPy,
|
||||
SuperBlocks.InfoSec,
|
||||
SuperBlocks.MachineLearningPy,
|
||||
SuperBlocks.CollegeAlgebraPy,
|
||||
SuperBlocks.RespWebDesign,
|
||||
SuperBlocks.JsAlgoDataStruct,
|
||||
SuperBlocks.PythonForEverybody
|
||||
],
|
||||
[SuperBlockStage.Next]: [],
|
||||
[SuperBlockStage.Upcoming]: [
|
||||
SuperBlocks.FullStackOpen,
|
||||
SuperBlocks.A2Spanish,
|
||||
SuperBlocks.A2Chinese,
|
||||
SuperBlocks.DevPlayground
|
||||
],
|
||||
// Catalog is treated like upcoming for now
|
||||
// Add catalog superBlocks to catalog.ts when adding new superBlocks
|
||||
[SuperBlockStage.Catalog]: [
|
||||
SuperBlocks.HtmlFormsAndTables,
|
||||
SuperBlocks.BasicHtml,
|
||||
SuperBlocks.SemanticHtml,
|
||||
SuperBlocks.HtmlAndAccessibility,
|
||||
SuperBlocks.ComputerBasics,
|
||||
SuperBlocks.BasicCss,
|
||||
SuperBlocks.DesignForDevelopers,
|
||||
SuperBlocks.AbsoluteAndRelativeUnits,
|
||||
SuperBlocks.PseudoClassesAndElements,
|
||||
SuperBlocks.CssColors,
|
||||
SuperBlocks.StylingForms,
|
||||
SuperBlocks.CssBoxModel,
|
||||
SuperBlocks.CssFlexbox,
|
||||
SuperBlocks.CssTypography,
|
||||
SuperBlocks.CssAndAccessibility,
|
||||
SuperBlocks.CssPositioning,
|
||||
SuperBlocks.AttributeSelectors,
|
||||
SuperBlocks.ResponsiveDesign,
|
||||
SuperBlocks.CssVariables,
|
||||
SuperBlocks.CssGrid,
|
||||
SuperBlocks.CssAnimations,
|
||||
SuperBlocks.LearnPythonForBeginners,
|
||||
SuperBlocks.IntroductionToAlgorithmsAndDataStructures,
|
||||
SuperBlocks.LearnOOPWithPython,
|
||||
SuperBlocks.LearnRAGAndMCPFundamentals,
|
||||
SuperBlocks.IntroductionToPrecalculus,
|
||||
SuperBlocks.IntroductionToBash,
|
||||
SuperBlocks.IntroductionToSQLAndPostgreSQL,
|
||||
SuperBlocks.LearnBashScripting,
|
||||
SuperBlocks.LearnSQLAndBash,
|
||||
SuperBlocks.IntroductionToNano,
|
||||
SuperBlocks.IntroductionToGitAndGithub,
|
||||
SuperBlocks.IntroductionToVariablesAndStringsInJS,
|
||||
SuperBlocks.IntroductionToBooleansAndNumbersInJS,
|
||||
SuperBlocks.IntroductionToFunctionsInJS,
|
||||
SuperBlocks.IntroductionToArraysInJS,
|
||||
SuperBlocks.IntroductionToObjectsInJS,
|
||||
SuperBlocks.IntroductionToLoopsInJS,
|
||||
SuperBlocks.JavascriptFundamentalsReview,
|
||||
SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS,
|
||||
SuperBlocks.LearnDomManipulationAndEventsWithJS,
|
||||
SuperBlocks.IntroductionToJavascriptAndAccessibility,
|
||||
SuperBlocks.LearnJavascriptDebugging,
|
||||
SuperBlocks.LearnBasicRegexWithJS,
|
||||
SuperBlocks.IntroductionToDatesInJS,
|
||||
SuperBlocks.LearnAudioAndVideoEventsWithJS,
|
||||
SuperBlocks.IntroductionToMapsAndSetsInJS,
|
||||
SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS,
|
||||
SuperBlocks.IntroductionToJavascriptClasses,
|
||||
SuperBlocks.LearnRecursionWithJS,
|
||||
SuperBlocks.IntroductionToFunctionalProgrammingWithJS,
|
||||
SuperBlocks.IntroductionToAsynchronousJS,
|
||||
SuperBlocks.LearnDataVisualizationWithD3,
|
||||
SuperBlocks.IntroductionToPythonBasics,
|
||||
SuperBlocks.LearnPythonLoopsAndSequences,
|
||||
SuperBlocks.LearnPythonDictionariesAndSets,
|
||||
SuperBlocks.LearnErrorHandlingInPython,
|
||||
SuperBlocks.LearnPythonClassesAndObjects,
|
||||
SuperBlocks.IntroductionToOOPInPython,
|
||||
SuperBlocks.IntroductionToLinearDataStructuresInPython,
|
||||
SuperBlocks.LearnAlgorithmsInPython,
|
||||
SuperBlocks.LearnGraphsAndTreesInPython,
|
||||
SuperBlocks.LearnDynamicProgrammingInPython
|
||||
]
|
||||
};
|
||||
|
||||
Object.freeze(superBlockStages);
|
||||
|
||||
export const archivedSuperBlocks = superBlockStages[SuperBlockStage.Legacy];
|
||||
|
||||
type NotAuditedSuperBlocks = {
|
||||
[key in Languages]: SuperBlocks[];
|
||||
};
|
||||
|
||||
// when a superBlock is audited, remove it from its language below
|
||||
// when adding a new language, add all (not audited) superblocks to the object
|
||||
export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
||||
[Languages.English]: [],
|
||||
[Languages.Espanol]: [],
|
||||
[Languages.Chinese]: [],
|
||||
[Languages.ChineseTraditional]: [],
|
||||
[Languages.Italian]: [],
|
||||
[Languages.Portuguese]: [],
|
||||
[Languages.Ukrainian]: [],
|
||||
[Languages.Japanese]: [],
|
||||
[Languages.German]: [],
|
||||
[Languages.Swahili]: [],
|
||||
[Languages.Korean]: []
|
||||
};
|
||||
|
||||
Object.freeze(notAuditedSuperBlocks);
|
||||
|
||||
export const chapterBasedSuperBlocks = [
|
||||
SuperBlocks.FullStackOpen,
|
||||
SuperBlocks.A1Spanish,
|
||||
SuperBlocks.RespWebDesignV9,
|
||||
SuperBlocks.JsV9,
|
||||
SuperBlocks.FrontEndDevLibsV9,
|
||||
SuperBlocks.PythonV9,
|
||||
SuperBlocks.RelationalDbV9,
|
||||
SuperBlocks.BackEndDevApisV9,
|
||||
SuperBlocks.FullStackDeveloperV9,
|
||||
SuperBlocks.LearnDataVisualizationWithD3,
|
||||
SuperBlocks.A1Chinese
|
||||
];
|
||||
Object.freeze(chapterBasedSuperBlocks);
|
||||
|
||||
export const certificationCollectionSuperBlocks = [
|
||||
SuperBlocks.FullStackDeveloperV9
|
||||
];
|
||||
Object.freeze(certificationCollectionSuperBlocks);
|
||||
|
||||
type Config = {
|
||||
showUpcomingChanges: boolean;
|
||||
};
|
||||
|
||||
export function generateSuperBlockList(config: Config): SuperBlocks[] {
|
||||
return getStageOrder(config)
|
||||
.map(stage => superBlockStages[stage])
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function getAuditedSuperBlocks({
|
||||
language = 'english'
|
||||
}: {
|
||||
language: string;
|
||||
}): SuperBlocks[] {
|
||||
if (!Object.prototype.hasOwnProperty.call(notAuditedSuperBlocks, language)) {
|
||||
throw Error(`'${language}' key not found in 'notAuditedSuperBlocks'`);
|
||||
}
|
||||
|
||||
// To find the audited superblocks, we need to start with all superblocks.
|
||||
const flatSuperBlockMap = generateSuperBlockList({
|
||||
showUpcomingChanges: true
|
||||
});
|
||||
const auditedSuperBlocks = flatSuperBlockMap.filter(
|
||||
superBlock =>
|
||||
!notAuditedSuperBlocks[language as Languages].includes(superBlock)
|
||||
);
|
||||
return auditedSuperBlocks;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Configuration for client side
|
||||
|
||||
export type DonationAmount = 500 | 1000 | 2000 | 2500 | 4000;
|
||||
export type DonationDuration = 'one-time' | 'month';
|
||||
export interface DonationConfig {
|
||||
donationAmount: DonationAmount;
|
||||
donationDuration: DonationDuration;
|
||||
}
|
||||
|
||||
export const subscriptionAmounts: DonationAmount[] = [500, 1000, 2000, 4000];
|
||||
export const subscriptionAmountsB: DonationAmount[] = [500, 1000, 2500, 4000];
|
||||
|
||||
export const defaultDonation: DonationConfig = {
|
||||
donationAmount: 500,
|
||||
donationDuration: 'month'
|
||||
};
|
||||
|
||||
export const defaultTierAmount: DonationAmount = 2000;
|
||||
export const defaultTierAmountB: DonationAmount = 2500;
|
||||
|
||||
export const onetimeSKUConfig = {
|
||||
production: [
|
||||
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
|
||||
{ amount: '10000', id: 'sku_IEliodY88lglPk' },
|
||||
{ amount: '7500', id: 'sku_IEli9AXW8DwNtT' },
|
||||
{ amount: '5000', id: 'sku_IElhJxkNh9UgDp' },
|
||||
{ amount: '2500', id: 'sku_IElhQtqLgKZC8y' }
|
||||
],
|
||||
staging: [
|
||||
{ amount: '15000', id: 'sku_IEPNpHACYJmUwz' },
|
||||
{ amount: '10000', id: 'sku_IEPMY1OXxnY4WU' },
|
||||
{ amount: '7500', id: 'sku_IEPLOotEqlMOWC' },
|
||||
{ amount: '5000', id: 'sku_IEPKAxxAxfMnUI' },
|
||||
{ amount: '2500', id: 'sku_IEPIgLRzViwq5z' }
|
||||
]
|
||||
};
|
||||
|
||||
// Configuration for server side
|
||||
export const durationKeysConfig = ['month', 'one-time'];
|
||||
export const donationOneTimeConfig = [100000, 25000, 6000];
|
||||
export const donationSubscriptionConfig = {
|
||||
duration: {
|
||||
month: 'monthly'
|
||||
},
|
||||
plans: {
|
||||
month: subscriptionAmounts
|
||||
}
|
||||
};
|
||||
|
||||
// Shared paypal configuration
|
||||
// keep the 5 dollars for the modal
|
||||
export const paypalConfigTypes = {
|
||||
production: {
|
||||
month: {
|
||||
500: { planId: 'P-6B636789V3105190KMTJFH7A' },
|
||||
1000: { planId: 'P-53P76823N8780520DMVTWF3I' },
|
||||
2000: { planId: 'P-8HY47434FB9663500MVTWFOA' },
|
||||
2500: { planId: 'P-1E758922LA293854BNC3SK3A' },
|
||||
3000: { planId: 'P-1KY930839N8045117L6E4BKY' },
|
||||
4000: { planId: 'P-0MH28916302828423MVTWEBI' },
|
||||
5000: { planId: 'P-0WR49877YD949401BL6E4CTA' }
|
||||
}
|
||||
},
|
||||
staging: {
|
||||
month: {
|
||||
500: { planId: 'P-37N14480BW163382FLZYPVMA' },
|
||||
1000: { planId: 'P-28B62039J8092810UL6E3FXA' },
|
||||
2000: { planId: 'P-7HR706961M9170433L6HI5VI' },
|
||||
2500: { planId: 'P-2BK29709FB733490FNC3RPGQ' },
|
||||
3000: { planId: 'P-35V33574BU596924JL6HI6XY' },
|
||||
4000: { planId: 'P-45M45060289267734L6HJSXA' },
|
||||
5000: { planId: 'P-0MD70861FY4172444L6HJTUQ' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface OneTimeConfig {
|
||||
amount: DonationAmount;
|
||||
duration: 'one-time';
|
||||
planId: null;
|
||||
}
|
||||
|
||||
interface SubscriptionConfig {
|
||||
amount: DonationAmount;
|
||||
duration: 'month';
|
||||
planId: string;
|
||||
}
|
||||
|
||||
export const paypalConfigurator = (
|
||||
donationAmount: DonationAmount,
|
||||
donationDuration: 'one-time' | 'month',
|
||||
paypalConfig: {
|
||||
month: {
|
||||
500: { planId: string };
|
||||
1000: { planId: string };
|
||||
2000: { planId: string };
|
||||
2500: { planId: string };
|
||||
3000: { planId: string };
|
||||
4000: { planId: string };
|
||||
5000: { planId: string };
|
||||
};
|
||||
}
|
||||
): OneTimeConfig | SubscriptionConfig => {
|
||||
if (donationDuration === 'one-time') {
|
||||
return { amount: donationAmount, duration: donationDuration, planId: null };
|
||||
}
|
||||
return {
|
||||
amount: donationAmount,
|
||||
duration: donationDuration,
|
||||
planId: paypalConfig[donationDuration][donationAmount].planId
|
||||
};
|
||||
};
|
||||
|
||||
export const donationUrls = {
|
||||
successUrl: 'https://www.freecodecamp.org/news/thank-you-for-donating/',
|
||||
cancelUrl: 'https://freecodecamp.org/donate'
|
||||
};
|
||||
|
||||
export enum PaymentContext {
|
||||
Modal = 'modal',
|
||||
DonatePage = 'donate page',
|
||||
Certificate = 'certificate'
|
||||
}
|
||||
|
||||
export enum PaymentProvider {
|
||||
Paypal = 'paypal',
|
||||
Patreon = 'patreon',
|
||||
Stripe = 'stripe',
|
||||
StripeCard = 'stripe card'
|
||||
}
|
||||
|
||||
const stripeProductIds = {
|
||||
production: {
|
||||
month: {
|
||||
500: 'prod_Cc9bIxB2NvjpLy',
|
||||
1000: 'prod_BuiSxWk7jGSFlJ',
|
||||
2000: 'prod_IElpZVK7kOn6Fe',
|
||||
2500: 'prod_JCakZSxh12ZaDF',
|
||||
4000: 'prod_IElq1foW39g3Cx'
|
||||
}
|
||||
},
|
||||
staging: {
|
||||
month: {
|
||||
500: 'prod_GD1GGbJsqQaupl',
|
||||
1000: 'prod_GD1IzNEXfSCGgy',
|
||||
2000: 'prod_IEkNp8M03xvsuB',
|
||||
2500: 'prod_T12UtcRPvzzVN1',
|
||||
4000: 'prod_IEkPebxS63mVbs'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const allStripeProductIdsArray = [
|
||||
...Object.values(stripeProductIds['production']['month']),
|
||||
...Object.values(stripeProductIds['staging']['month'])
|
||||
];
|
||||
@@ -0,0 +1,130 @@
|
||||
export enum Languages {
|
||||
English = 'english',
|
||||
Espanol = 'espanol',
|
||||
Chinese = 'chinese',
|
||||
ChineseTraditional = 'chinese-traditional',
|
||||
Italian = 'italian',
|
||||
Portuguese = 'portuguese',
|
||||
Ukrainian = 'ukrainian',
|
||||
Japanese = 'japanese',
|
||||
German = 'german',
|
||||
Swahili = 'swahili',
|
||||
Korean = 'korean'
|
||||
}
|
||||
|
||||
/*
|
||||
* List of languages with localizations enabled for builds.
|
||||
*
|
||||
* Client is the UI, and Curriculum is the Challenge Content.
|
||||
*
|
||||
* An error will be thrown if the CLIENT_LOCALE and CURRICULUM_LOCALE variables
|
||||
* from the .env file aren't found in their respective arrays below
|
||||
*/
|
||||
export const availableLangs = {
|
||||
client: [
|
||||
Languages.English,
|
||||
Languages.Espanol,
|
||||
Languages.Chinese,
|
||||
Languages.ChineseTraditional,
|
||||
Languages.Italian,
|
||||
Languages.Portuguese,
|
||||
Languages.Ukrainian,
|
||||
Languages.Japanese,
|
||||
Languages.German,
|
||||
Languages.Swahili,
|
||||
Languages.Korean
|
||||
],
|
||||
curriculum: [
|
||||
Languages.English,
|
||||
Languages.Espanol,
|
||||
Languages.Chinese,
|
||||
Languages.ChineseTraditional,
|
||||
Languages.Italian,
|
||||
Languages.Portuguese,
|
||||
Languages.Ukrainian,
|
||||
Languages.Japanese,
|
||||
Languages.German,
|
||||
Languages.Swahili,
|
||||
Languages.Korean
|
||||
]
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Each client language needs an entry in the rest of the variables below
|
||||
|
||||
/* These strings set the i18next language. It needs to be the two character
|
||||
* string for the language to take advantage of available functionality.
|
||||
* Use a 639-1 code here https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
*/
|
||||
export const i18nextCodes = {
|
||||
[Languages.English]: 'en',
|
||||
[Languages.Espanol]: 'es',
|
||||
[Languages.Chinese]: 'zh',
|
||||
[Languages.ChineseTraditional]: 'zh-Hant',
|
||||
[Languages.Italian]: 'it',
|
||||
[Languages.Portuguese]: 'pt-BR',
|
||||
[Languages.Ukrainian]: 'uk',
|
||||
[Languages.Japanese]: 'ja',
|
||||
[Languages.German]: 'de',
|
||||
[Languages.Swahili]: 'sw',
|
||||
[Languages.Korean]: 'ko'
|
||||
};
|
||||
|
||||
// These are for the language selector dropdown menu in the footer
|
||||
export const LangNames: { [key: string]: string } = {
|
||||
[Languages.English]: 'English',
|
||||
[Languages.Espanol]: 'Español',
|
||||
[Languages.Chinese]: '中文(简体字)',
|
||||
[Languages.ChineseTraditional]: '中文(繁體字)',
|
||||
[Languages.Italian]: 'Italiano',
|
||||
[Languages.Portuguese]: 'Português',
|
||||
[Languages.Ukrainian]: 'Українська',
|
||||
[Languages.Japanese]: '日本語',
|
||||
[Languages.German]: 'Deutsch',
|
||||
[Languages.Swahili]: 'Swahili',
|
||||
[Languages.Korean]: '한국어'
|
||||
};
|
||||
|
||||
/* These are for formatting dates and numbers. Used with JS .toLocaleString().
|
||||
* There's an example in profile/components/Camper.js
|
||||
* List: https://github.com/unicode-cldr/cldr-dates-modern/tree/master/main
|
||||
*/
|
||||
export const LangCodes = {
|
||||
[Languages.English]: 'en-US',
|
||||
[Languages.Espanol]: 'es-419',
|
||||
[Languages.Chinese]: 'zh',
|
||||
[Languages.ChineseTraditional]: 'zh-Hant',
|
||||
[Languages.Italian]: 'it',
|
||||
[Languages.Portuguese]: 'pt-BR',
|
||||
[Languages.Ukrainian]: 'uk',
|
||||
[Languages.Japanese]: 'ja',
|
||||
[Languages.German]: 'de',
|
||||
[Languages.Swahili]: 'sw',
|
||||
[Languages.Korean]: 'ko'
|
||||
};
|
||||
|
||||
/**
|
||||
* This array contains languages that should NOT appear in the language selector.
|
||||
*/
|
||||
export const hiddenLangs: Languages[] = [];
|
||||
|
||||
/**
|
||||
* This array contains languages that use the RTL layouts.
|
||||
*/
|
||||
export const rtlLangs: Languages[] = [];
|
||||
|
||||
// locale is sourced from a JSON file, so we use getLangCode to
|
||||
// find the associated enum values
|
||||
|
||||
export function getLangCode(locale: PropertyKey): string {
|
||||
if (isPropertyOf(LangCodes, locale)) return LangCodes[locale];
|
||||
throw new Error(`${String(locale)} is not a valid locale`);
|
||||
}
|
||||
|
||||
function isPropertyOf<O>(
|
||||
obj: Record<string, string>,
|
||||
key: PropertyKey
|
||||
): key is keyof O {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { BlockLabel } from './blocks';
|
||||
|
||||
export interface Module {
|
||||
dashedName: string;
|
||||
comingSoon?: boolean;
|
||||
blocks: string[];
|
||||
moduleType?: BlockLabel;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getLines } from './get-lines';
|
||||
|
||||
const content = 'one\ntwo\nthree';
|
||||
|
||||
describe('dasherize', () => {
|
||||
it('returns a string', () => {
|
||||
expect(getLines('')).toBe('');
|
||||
});
|
||||
it("returns '' when the second arg is empty", () => {
|
||||
expect(getLines(content)).toBe('');
|
||||
});
|
||||
it("returns '' when the range is negative", () => {
|
||||
expect(getLines(content, [1, -1])).toBe('');
|
||||
});
|
||||
it("returns '' when the range is [n,n]", () => {
|
||||
expect(getLines(content, [0, 0])).toBe('');
|
||||
expect(getLines(content, [1, 1])).toBe('');
|
||||
expect(getLines(content, [2, 2])).toBe('');
|
||||
});
|
||||
|
||||
it('returns the first line when the range is [0,2]', () => {
|
||||
expect(getLines(content, [0, 2])).toBe('one');
|
||||
});
|
||||
it('returns the second line when the range is [1,3]', () => {
|
||||
expect(getLines(content, [1, 3])).toBe('two');
|
||||
});
|
||||
it('returns the first and second lines when the range is [0,3]', () => {
|
||||
expect(getLines(content, [0, 3])).toBe('one\ntwo');
|
||||
});
|
||||
it('returns the second and third lines when the range is [1,4]', () => {
|
||||
expect(getLines(content, [1, 4])).toBe('two\nthree');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getLines(contents: string, range?: number[]): string {
|
||||
if (!range) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = contents.split('\n');
|
||||
const editableLines =
|
||||
range[1] <= range[0] ? [] : lines.slice(range[0], range[1] - 1);
|
||||
return editableLines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type SuperBlocks, getAuditedSuperBlocks } from '../config/curriculum';
|
||||
|
||||
export function isAuditedSuperBlock(language: string, superblock: SuperBlocks) {
|
||||
const auditedSuperBlocks = getAuditedSuperBlocks({
|
||||
language
|
||||
});
|
||||
return auditedSuperBlocks.includes(superblock);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPoly, createSource } from './polyvinyl';
|
||||
|
||||
const polyData = {
|
||||
name: 'test',
|
||||
ext: 'js',
|
||||
contents: 'var hello = world;',
|
||||
history: ['test.js']
|
||||
};
|
||||
|
||||
describe('createSource', () => {
|
||||
it('should return a vinyl object with a source matching the contents', () => {
|
||||
const original = createPoly(polyData);
|
||||
|
||||
const updated = createSource(original);
|
||||
expect(original).not.toHaveProperty('source');
|
||||
expect(updated).toHaveProperty('source', 'var hello = world;');
|
||||
expect(updated).toMatchObject(original);
|
||||
});
|
||||
|
||||
it('should not update the source if it already exists', () => {
|
||||
const original = createPoly({
|
||||
...polyData,
|
||||
source: 'const hello = world;'
|
||||
});
|
||||
|
||||
const updated = createSource(original);
|
||||
expect(updated).toStrictEqual(original);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'tsx', 'py', 'json'] as const;
|
||||
export type Ext = (typeof exts)[number];
|
||||
|
||||
export interface IncompleteChallengeFile {
|
||||
fileKey: string;
|
||||
ext: Ext;
|
||||
name: string;
|
||||
contents: string;
|
||||
}
|
||||
|
||||
export interface ChallengeFile extends IncompleteChallengeFile {
|
||||
editableRegionBoundaries?: number[];
|
||||
editableContents?: string;
|
||||
usesMultifileEditor?: boolean;
|
||||
error?: unknown;
|
||||
seed?: string;
|
||||
source?: string;
|
||||
path: string;
|
||||
history: string[];
|
||||
}
|
||||
|
||||
type PolyProps = {
|
||||
name: string;
|
||||
ext: string;
|
||||
contents: string;
|
||||
history?: string[];
|
||||
};
|
||||
|
||||
// The types are a little awkward, but should suffice until we move the
|
||||
// curriculum to TypeScript.
|
||||
type AddedProperties = {
|
||||
path: string;
|
||||
fileKey: string;
|
||||
error: null;
|
||||
};
|
||||
|
||||
export function createPoly<Rest>({
|
||||
name,
|
||||
ext,
|
||||
contents,
|
||||
history,
|
||||
...rest
|
||||
}: PolyProps & Rest): PolyProps & AddedProperties & Rest {
|
||||
if (typeof name !== 'string') throw new TypeError('name must be a string');
|
||||
if (typeof ext !== 'string') throw new TypeError('ext must be a string');
|
||||
if (typeof contents !== 'string')
|
||||
throw new TypeError('contents must be a string');
|
||||
|
||||
return {
|
||||
...rest,
|
||||
history: Array.isArray(history) ? history : [name + '.' + ext],
|
||||
name,
|
||||
ext,
|
||||
path: name + '.' + ext,
|
||||
fileKey: name + ext,
|
||||
contents,
|
||||
error: null
|
||||
} as PolyProps & AddedProperties & Rest;
|
||||
}
|
||||
|
||||
export function isPoly(poly: unknown): poly is ChallengeFile {
|
||||
function hasProperties(poly: unknown): poly is Record<string, unknown> {
|
||||
return (
|
||||
!!poly &&
|
||||
typeof poly === 'object' &&
|
||||
'contents' in poly &&
|
||||
'name' in poly &&
|
||||
'ext' in poly &&
|
||||
'fileKey' in poly &&
|
||||
'history' in poly
|
||||
);
|
||||
}
|
||||
|
||||
const hasCorrectTypes = (poly: Record<string, unknown>): boolean =>
|
||||
typeof poly.contents === 'string' &&
|
||||
typeof poly.name === 'string' &&
|
||||
exts.includes(poly.ext as Ext) &&
|
||||
typeof poly.fileKey === 'string' &&
|
||||
Array.isArray(poly.history);
|
||||
|
||||
return hasProperties(poly) && hasCorrectTypes(poly);
|
||||
}
|
||||
|
||||
function checkPoly(poly: ChallengeFile) {
|
||||
if (!isPoly(poly)) throw Error('Not a PolyVinyl: ' + JSON.stringify(poly));
|
||||
}
|
||||
|
||||
// setContent will lose source if not supplied
|
||||
export function setContent(
|
||||
contents: string,
|
||||
poly: ChallengeFile,
|
||||
source?: string
|
||||
): ChallengeFile {
|
||||
checkPoly(poly);
|
||||
return {
|
||||
...poly,
|
||||
contents,
|
||||
source
|
||||
};
|
||||
}
|
||||
|
||||
// This is currently only used to add back properties that are not stored in the
|
||||
// database.
|
||||
export function regenerateMissingProperties(file: IncompleteChallengeFile) {
|
||||
const newPath = file.name + '.' + file.ext;
|
||||
return {
|
||||
...file,
|
||||
path: newPath,
|
||||
history: [newPath]
|
||||
};
|
||||
}
|
||||
|
||||
type Wrapper = (x: string) => Promise<string> | string;
|
||||
// transformContents will keep a copy of the original
|
||||
// code in the `source` property. If the original polyvinyl
|
||||
// already contains a source, this version will continue as
|
||||
// the source property
|
||||
export async function transformContents(
|
||||
wrap: Wrapper,
|
||||
polyP: ChallengeFile | Promise<ChallengeFile>
|
||||
) {
|
||||
const poly = await polyP;
|
||||
const newPoly = setContent(await wrap(poly.contents), poly, poly.source);
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// createSource(poly: PolyVinyl) => PolyVinyl
|
||||
export function createSource<Rest>(
|
||||
poly: Pick<ChallengeFile, 'contents' | 'source'> & Rest
|
||||
): Rest & { contents: string; source: string } {
|
||||
return {
|
||||
...poly,
|
||||
source: poly.source ?? poly.contents
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Shuffle array using the Fisher–Yates shuffle algorithm */
|
||||
export const shuffleArray = <T>(arrToShuffle: Array<T>) => {
|
||||
const arr = [...arrToShuffle];
|
||||
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
|
||||
// We know that i and j are within the bounds of the array, TS doesn't
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
[arr[i], arr[j]] = [arr[j]!, arr[i]!];
|
||||
}
|
||||
|
||||
return arr;
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isValidUsername,
|
||||
usernameTooShort,
|
||||
validationSuccess,
|
||||
usernameIsHttpStatusCode,
|
||||
invalidCharError,
|
||||
isMicrosoftTranscriptLink
|
||||
} from './validate';
|
||||
|
||||
function inRange(num: number, range: number[]) {
|
||||
return num >= range[0] && num <= range[1];
|
||||
}
|
||||
|
||||
describe('isValidUsername', () => {
|
||||
it('rejects strings with less than 3 characters', () => {
|
||||
expect(isValidUsername('')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('12')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('a')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('12a')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('rejects strings which are http response status codes 100-599', () => {
|
||||
expect(isValidUsername('429')).toStrictEqual(usernameIsHttpStatusCode);
|
||||
expect(isValidUsername('789')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('rejects non-ASCII characters', () => {
|
||||
expect(isValidUsername('👀👂👄')).toStrictEqual(invalidCharError);
|
||||
});
|
||||
it('rejects with invalidCharError even if the string is too short', () => {
|
||||
expect(isValidUsername('.')).toStrictEqual(invalidCharError);
|
||||
});
|
||||
it('accepts alphanumeric characters', () => {
|
||||
expect(
|
||||
isValidUsername('abcdefghijklmnopqrstuvwxyz0123456789')
|
||||
).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('accepts hyphens and underscores', () => {
|
||||
expect(isValidUsername('a-b')).toStrictEqual(validationSuccess);
|
||||
expect(isValidUsername('a_b')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
|
||||
it('rejects all other ASCII characters', () => {
|
||||
const allowedCharactersList = ['-', '_', '+'];
|
||||
const numbers = [48, 57];
|
||||
const upperCase = [65, 90];
|
||||
const lowerCase = [97, 122];
|
||||
const base = 'user';
|
||||
const finalCode = 127;
|
||||
|
||||
for (let code = 0; code <= finalCode; code++) {
|
||||
const char = String.fromCharCode(code);
|
||||
let expected: { valid: boolean; error: null | string } = invalidCharError;
|
||||
if (allowedCharactersList.includes(char)) expected = validationSuccess;
|
||||
if (inRange(code, numbers)) expected = validationSuccess;
|
||||
if (inRange(code, upperCase)) expected = validationSuccess;
|
||||
if (inRange(code, lowerCase)) expected = validationSuccess;
|
||||
expect(isValidUsername(base + char)).toStrictEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const baseUrl = 'https://learn.microsoft.com/';
|
||||
|
||||
describe('isMicrosoftTranscriptLink', () => {
|
||||
it('should reject links to domains other than learn.microsoft.com', () => {
|
||||
{
|
||||
expect(isMicrosoftTranscriptLink('https://lean.microsoft.com')).toBe(
|
||||
false
|
||||
);
|
||||
expect(isMicrosoftTranscriptLink('https://learn.microsft.com')).toBe(
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject links without a username', () => {
|
||||
expect(isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links without a unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/moT01/transcript`)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links with anything after the unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`${baseUrl}/en-us/users/moT01/transcript/any-id/more-stuff`
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject the placeholder link', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID/'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['en-us', 'fr-fr', 'lang-country'])(
|
||||
'should accept links with the %s locale',
|
||||
locale => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`https://learn.microsoft.com/${locale}/users/moT01/transcript/any-id`
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
type Valid = {
|
||||
valid: true;
|
||||
error: null;
|
||||
};
|
||||
|
||||
type Invalid = {
|
||||
valid: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Validated = Valid | Invalid;
|
||||
|
||||
export const invalidCharError: Invalid = {
|
||||
valid: false,
|
||||
error: 'contains invalid characters'
|
||||
};
|
||||
export const validationSuccess: Valid = { valid: true, error: null };
|
||||
export const usernameTooShort: Invalid = {
|
||||
valid: false,
|
||||
error: 'is too short'
|
||||
};
|
||||
export const usernameIsHttpStatusCode: Invalid = {
|
||||
valid: false,
|
||||
error: 'is a reserved error code'
|
||||
};
|
||||
|
||||
const validCharsRE = /^[a-zA-Z0-9\-_+]*$/;
|
||||
export const isHttpStatusCode = (str: string): boolean => {
|
||||
const output = parseInt(str, 10);
|
||||
return !isNaN(output) && output >= 100 && output <= 599;
|
||||
};
|
||||
|
||||
export const isValidUsername = (str: string): Validated => {
|
||||
if (!validCharsRE.test(str)) return invalidCharError;
|
||||
if (str.length < 3) return usernameTooShort;
|
||||
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
|
||||
return validationSuccess;
|
||||
};
|
||||
|
||||
// link template:
|
||||
// https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID
|
||||
export const isMicrosoftTranscriptLink = (value: string): boolean => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const correctDomain = url.hostname === 'learn.microsoft.com';
|
||||
const correctPath = !!url.pathname.match(
|
||||
/^\/[^/]+\/users\/[^/]+\/transcript\/[^/]+$/
|
||||
);
|
||||
const notPlaceholder = !url.pathname.match(
|
||||
'/LOCALE/users/USERNAME/transcript/ID'
|
||||
);
|
||||
return correctDomain && correctPath && notPlaceholder;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"extends": "../../tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist",
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/**/*.ts', '!src/**/*.test.ts'],
|
||||
exports: true,
|
||||
dts: true
|
||||
});
|
||||
Reference in New Issue
Block a user