chore(learn): Merge learn in to the client app

This commit is contained in:
Bouncey
2018-09-30 11:37:19 +01:00
committed by Stuart Taylor
parent 9e869a46fc
commit 5b254f3ad6
320 changed files with 9820 additions and 27605 deletions

View File

@@ -0,0 +1,35 @@
const preFormattedBlockNames = {
'api-projects': 'API Projects',
'basic-css': 'Basic CSS',
'basic-html-and-html5': 'Basic HTML and HTML5',
'css-flexbox': 'CSS Flexbox',
'css-grid': 'CSS Grid',
devops: 'DevOps',
es6: 'ES6',
'information-security-with-helmetjs': 'Information Security with HelmetJS',
jquery: 'jQuery',
'json-apis-and-ajax': 'JSON APIs and Ajax',
'mongodb-and-mongoose': 'MongoDB and Mongoose',
'the-dom': 'The DOM'
};
const noFormatting = ['and', 'for', 'of', 'the', 'up', 'with'];
exports.blockNameify = function blockNameify(phrase) {
const preFormatted = preFormattedBlockNames[phrase] || '';
if (preFormatted) {
return preFormatted;
}
return phrase
.split('-')
.map(word => {
if (noFormatting.indexOf(word) !== -1) {
return word;
}
if (word === 'javascript') {
return 'JavaScript';
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
};

View File

@@ -0,0 +1,109 @@
const { getChallenges } = require('@freecodecamp/curriculum');
const { from, of } = require('rxjs');
const { map } = require('rxjs/operators');
const _ = require('lodash');
const utils = require('../utils');
const dasherize = utils.dasherize;
const nameify = utils.nameify;
const arrToString = arr =>
Array.isArray(arr) ? arr.join('\n') : _.toString(arr);
exports.buildChallenges$ = function buildChallenges$() {
return from(getChallenges()).pipe(
map(function(challengeSpec) {
const order = challengeSpec.order;
const blockName = challengeSpec.name;
const superBlock = challengeSpec.superBlock;
const superOrder = challengeSpec.superOrder;
const isBeta = !!challengeSpec.isBeta;
const isComingSoon = !!challengeSpec.isComingSoon;
const fileName = challengeSpec.fileName;
const helpRoom = challengeSpec.helpRoom || 'Help';
const time = challengeSpec.time;
const isLocked = !!challengeSpec.isLocked;
const message = challengeSpec.message;
const required = challengeSpec.required || [];
const template = challengeSpec.template;
const isPrivate = !!challengeSpec.isPrivate;
// challenge file has no challenges...
if (challengeSpec.challenges.length === 0) {
return of([{ block: 'empty ' + blockName }]);
}
const block = {
title: blockName,
name: nameify(blockName),
dashedName: dasherize(blockName),
superOrder,
superBlock,
superBlockMessage: message,
order,
time,
isLocked,
isPrivate
};
return challengeSpec.challenges.map(function(challenge, index) {
challenge.name = nameify(challenge.title);
challenge.dashedName = dasherize(challenge.name);
if (challenge.files) {
challenge.files = _.reduce(
challenge.files,
(map, file) => {
map[file.key] = {
...file,
head: arrToString(file.head),
contents: arrToString(file.contents),
tail: arrToString(file.tail)
};
return map;
},
{}
);
}
challenge.fileName = fileName;
challenge.helpRoom = helpRoom;
challenge.order = order;
challenge.suborder = index + 1;
challenge.block = dasherize(blockName);
challenge.blockId = block.id;
challenge.isBeta = challenge.isBeta || isBeta;
challenge.isComingSoon = challenge.isComingSoon || isComingSoon;
challenge.isLocked = challenge.isLocked || isLocked;
challenge.isPrivate = challenge.isPrivate || isPrivate;
challenge.isRequired = !!challenge.isRequired;
challenge.time = challengeSpec.time;
challenge.superOrder = superOrder;
challenge.superBlock = superBlock
.split('-')
.map(function(word) {
return _.capitalize(word);
})
.join(' ');
challenge.required = (challenge.required || []).concat(required);
challenge.template = challenge.template || template;
return _.omit(challenge, [
'betaSolutions',
'betaTests',
'hints',
'MDNlinks',
'null',
'rawSolutions',
'react',
'reactRedux',
'redux',
'releasedOn',
'translations',
'type'
]);
});
})
);
};

View File

@@ -0,0 +1,75 @@
const html = 0;
const js = 1;
const backend = 2;
const zipline = 3;
const frontEndProject = 3;
const backEndProject = 4;
const bonfire = 5;
const modern = 6;
const step = 7;
const quiz = 8;
const invalid = 9;
// individual exports
exports.backend = backend;
exports.frontEndProject = frontEndProject;
exports.challengeTypes = {
html,
js,
backend,
zipline,
frontEndProject,
backEndProject,
bonfire,
modern,
step,
quiz,
invalid
};
// turn challengeType to file ext
exports.pathsMap = {
[html]: 'html',
[js]: 'js',
[bonfire]: 'js'
};
// determine the component to view for each challenge
exports.viewTypes = {
[html]: 'classic',
[js]: 'classic',
[bonfire]: 'classic',
[frontEndProject]: 'project',
[backEndProject]: 'project',
[modern]: 'modern',
[step]: 'step',
[quiz]: 'quiz',
[backend]: 'backend'
};
// determine the type of submit function to use for the challenge on completion
exports.submitTypes = {
[html]: 'tests',
[js]: 'tests',
[bonfire]: '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',
[step]: 'step',
[quiz]: 'quiz',
[backend]: 'backend',
[modern]: 'tests'
};
// determine which help forum questions should be posted to
exports.helpCategory = {
[html]: 'HTML-CSS',
[js]: 'JavaScript',
[backend]: 'JavaScript',
[modern]: 'JavaScript'
};

View File

@@ -0,0 +1,27 @@
/*
* Converts HTML entity codes in a string to the characters they represent.
*
* Example:
* `decodeHTMLEntities('Beets & carrots');`
* will return "Beets & carrots".
*
* The regex makes sure we only replace the HTML entities in the string.
* For example, the regex would match "<" as well as ":".
* The decoding works by setting the innerHTML of a dummy element and then
* retrieving the innerText. Per the spec, innerText is a property that
* represents the "rendered" text content of an element.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/Node/innerText
* https://developer.mozilla.org/en-US/docs/Glossary/Entity
*
*/
const decodeHTMLEntities = str => {
const el = document.createElement('div');
return str.replace(/&[#0-9a-z]+;/gi, enc => {
el.innerHTML = enc;
return el.innerText;
});
};
export default decodeHTMLEntities;

View File

@@ -0,0 +1,103 @@
const { dasherize } = require('..');
const path = require('path');
const { viewTypes } = require('../challengeTypes');
const backend = path.resolve(
__dirname,
'../../src/templates/Challenges/backend/Show.js'
);
const classic = path.resolve(
__dirname,
'../../src/templates/Challenges/classic/Show.js'
);
const project = path.resolve(
__dirname,
'../../src/templates/Challenges/project/Show.js'
);
const intro = path.resolve(
__dirname,
'../../src/templates/Introduction/Intro.js'
);
const superBlockIntro = path.resolve(
__dirname,
'../../src/templates/Introduction/SuperBlockIntro.js'
);
const views = {
backend,
classic,
modern: classic,
project
// quiz: Quiz
};
const getNextChallengePath = (node, index, nodeArray) => {
const next = nodeArray[index + 1];
return next ? next.node.fields.slug : '/';
};
const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
const getIntroIfRequired = (node, index, nodeArray) => {
const next = nodeArray[index + 1];
const isEndOfBlock = next && next.node.suborder === 1;
let nextSuperBlock = '';
let nextBlock = '';
if (next) {
const { superBlock, block } = next.node;
nextSuperBlock = superBlock;
nextBlock = block;
}
return isEndOfBlock
? `/${dasherize(nextSuperBlock)}/${dasherize(nextBlock)}`
: '';
};
exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
const { fields: { slug }, required = [], template, challengeType, id } = node;
if (challengeType === 7) {
return;
}
createPage({
path: slug,
component: getTemplateComponent(challengeType),
context: {
challengeMeta: {
introPath: getIntroIfRequired(node, index, thisArray),
template,
required,
nextChallengePath: getNextChallengePath(node, index, thisArray),
id
},
slug
}
});
};
exports.createIntroPages = createPage => edge => {
const { fields: { slug }, frontmatter: { superBlock, block } } = edge.node;
// If there is no block specified in the markdown we assume the markdown is
// for a superblock introduction. Otherwise create a block intro page.
if (!block) {
createPage({
path: slug,
component: superBlockIntro,
context: {
superBlock: dasherize(superBlock),
slug
}
});
} else {
createPage({
path: slug,
component: intro,
context: {
block: dasherize(block),
slug
}
});
}
};

29
client/utils/index.js Normal file
View File

@@ -0,0 +1,29 @@
exports.dasherize = function dasherize(name) {
return ('' + name)
.toLowerCase()
.replace(/\s/g, '-')
.replace(/[^a-z0-9\-.]/gi, '')
.replace(/\./g, '-')
.replace(/:/g, '');
};
exports.nameify = function nameify(str) {
return ('' + str).replace(/[^a-zA-Z0-9\s]/g, '').replace(/:/g, '');
};
exports.unDasherize = function unDasherize(name) {
return (
('' + name)
// replace dash with space
.replace(/-/g, ' ')
// strip nonalphanumarics chars except whitespace
.replace(/[^a-zA-Z\d\s]/g, '')
.trim()
);
};
exports.descriptionRegex = /<blockquote|<ol|<h4|<table/;
exports.isBrowser = function isBrowser() {
return typeof window !== 'undefined';
};

View File

@@ -0,0 +1,9 @@
export function createTypes(types = [], ns = 'annon') {
return types.reduce(
(types, action) => ({
...types,
[action]: `${ns}.${action}`
}),
{}
);
}