chore(learn): Merge learn in to the client app
This commit is contained in:
35
client/utils/blockNameify.js
Normal file
35
client/utils/blockNameify.js
Normal 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(' ');
|
||||
};
|
109
client/utils/buildChallenges.js
Normal file
109
client/utils/buildChallenges.js
Normal 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'
|
||||
]);
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
75
client/utils/challengeTypes.js
Normal file
75
client/utils/challengeTypes.js
Normal 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'
|
||||
};
|
27
client/utils/decodeHTMLEntities.js
Normal file
27
client/utils/decodeHTMLEntities.js
Normal 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;
|
103
client/utils/gatsby/index.js
Normal file
103
client/utils/gatsby/index.js
Normal 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
29
client/utils/index.js
Normal 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';
|
||||
};
|
9
client/utils/stateManagement.js
Normal file
9
client/utils/stateManagement.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function createTypes(types = [], ns = 'annon') {
|
||||
return types.reduce(
|
||||
(types, action) => ({
|
||||
...types,
|
||||
[action]: `${ns}.${action}`
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user