Merge pull request #18 from Bouncey/feat/transitionIntros

Feat Transition Intros
This commit is contained in:
Stuart Taylor 2018-04-17 15:24:17 +01:00 committed by Mrugesh Mohapatra
parent fa68757553
commit 4d9f59f318
12 changed files with 243 additions and 85 deletions

View File

@ -2,30 +2,8 @@ const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { dasherize } = require('./utils');
const { viewTypes } = require('./utils/challengeTypes');
const { blockNameify } = require('./utils/blockNameify');
const backend = path.resolve(
__dirname,
'./src/templates/Challenges/backend/Show.js'
);
const classic = path.resolve(
__dirname,
'./src/templates/Challenges/classic/Show.js'
);
const intro = path.resolve(__dirname, './src/templates/Introduction/Intro.js');
const project = path.resolve(
__dirname,
'./src/templates/Challenges/project/Show.js'
);
const views = {
backend,
classic,
modern: classic,
project
// quiz: Quiz
};
const { createChallengePages, createIntroPages } = require('./utils/gatsby');
exports.onCreateNode = function onCreateNode({ node, boundActionCreators }) {
const { createNodeField } = boundActionCreators;
@ -60,17 +38,22 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
allChallengeNode(sort: { fields: [superOrder, order, suborder] }) {
edges {
node {
block
challengeType
fields {
slug
}
id
order
required {
link
raw
src
}
suborder
superBlock
superOrder
template
fields {
slug
}
}
}
}
@ -97,46 +80,14 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
}
// Create challenge pages.
result.data.allChallengeNode.edges.forEach((edge, index, thisArray) => {
const {
fields: { slug },
required = [],
template,
challengeType,
id
} = edge.node;
if (challengeType === 7) {
return;
}
const next = thisArray[index + 1];
const nextChallengePath = next ? next.node.fields.slug : '/';
createPage({
path: slug,
component: views[viewTypes[challengeType]],
context: {
challengeMeta: {
challengeType,
template,
required,
nextChallengePath,
id
},
slug
}
});
});
result.data.allChallengeNode.edges.forEach(
createChallengePages(createPage)
);
// Create intro pages
result.data.allMarkdownRemark.edges.forEach(edge => {
const { fields: { slug } } = edge.node;
createPage({
path: slug,
component: intro,
context: {
slug
}
});
});
result.data.allMarkdownRemark.edges.forEach(
createIntroPages(createPage)
);
return;
})

View File

@ -55,12 +55,14 @@
"build:frame-runner": "webpack --config ./webpack-frame-runner.js",
"build:loop-protect": "webpack --config ./webpack-loop-protect.js",
"develop": "yarn build:frame-runner && gatsby develop",
"format": "yarn format:gatsby && yarn format:src && yarn lint",
"format": "yarn format:gatsby && yarn format:src && yarn format:utils && yarn lint",
"format:gatsby": "prettier --write './gatsby*.js'",
"format:src": "prettier --write './src/**/*.js'",
"lint": "yarn lint:gatsby && yarn lint:src",
"format:utils": "prettier --write './utils/**/*.js'",
"lint": "yarn lint:gatsby && yarn lint:src && yarn lint:utils",
"lint:gatsby": "eslint ./gatsby*.js --fix",
"lint:src": "eslint ./src . --fix",
"lint:utils": "eslint ./utils . --fix",
"pretty": "yarn format && yarn lint",
"test": "yarn format && jest src",
"test:watch": "jest --watch src"

View File

@ -1,5 +1,7 @@
import React from 'react';
import './button-spacer.css';
function ButtonSpacer() {
return <div className='button-spacer' />;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'react-bootstrap';
function FullWidthRow({ children }) {
return (
<Row>
<Col sm={8} smOffset={2} xs={12}>
{children}
</Col>
</Row>
);
}
FullWidthRow.displayName = 'FullWidthRow';
FullWidthRow.propTypes = {
children: PropTypes.any
};
export default FullWidthRow;

View File

@ -0,0 +1,16 @@
/* global expect */
import React from 'react';
import renderer from 'react-test-renderer';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import FullWidthRow from './FullWidthRow';
Enzyme.configure({ adapter: new Adapter() });
test('<FullWidthRow /> snapshot', () => {
const component = renderer.create(<FullWidthRow />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FullWidthRow /> snapshot 1`] = `
<div
className="row"
>
<div
className="col-sm-8 col-sm-offset-2 col-xs-12"
/>
</div>
`;

View File

@ -0,0 +1,3 @@
.button-spacer {
margin: 15px 0;
}

View File

@ -162,13 +162,18 @@ export default function completionEpic(action$, { getState }) {
return action$.pipe(
ofType(types.submitChallenge),
switchMap(({ type }) => {
const { nextChallengePath } = challengeMetaSelector(getState());
const { nextChallengePath, introPath } = challengeMetaSelector(
getState()
);
// const state = getState();
// const { submitType } = challengeMetaSelector(state);
// const submitter = submitters[submitType] || (() => Observable.empty());
return type === types.submitChallenge
? of(closeModal('completion'), push(nextChallengePath))
? of(
closeModal('completion'),
push(introPath ? introPath : nextChallengePath)
)
: of({ type: 'PONG' });
})
);

View File

@ -1,18 +1,62 @@
/* global graphql */
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { MarkdownRemark } from '../../redux/propTypes';
import Link from 'gatsby-link';
import Helmet from 'react-helmet';
import { Button, ListGroup, ListGroupItem } from 'react-bootstrap';
import FullWidthRow from '../../components/util/FullWidthRow';
import ButtonSpacer from '../../components/util/ButtonSpacer';
import { MarkdownRemark, AllChallengeNode } from '../../redux/propTypes';
import './intro.css';
const propTypes = {
data: PropTypes.shape({
markdownRemark: MarkdownRemark
markdownRemark: MarkdownRemark,
allChallengeNode: AllChallengeNode
})
};
function IntroductionPage({ data: { markdownRemark } }) {
const { html } = markdownRemark;
function renderMenuItems({ edges }) {
return edges.map(({ node }) => node).map(({ title, fields: { slug } }) => (
<Link to={slug}>
<ListGroupItem>{title}</ListGroupItem>
</Link>
));
}
function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
const { html, frontmatter: { block } } = markdownRemark;
const firstLesson = allChallengeNode.edges[0].node;
const firstLessonPath = firstLesson.fields.slug;
return (
<div className='intro-layout' dangerouslySetInnerHTML={{ __html: html }} />
<Fragment>
<Helmet>
<title>{block} | freeCodeCamp</title>
</Helmet>
<FullWidthRow>
<div
className='intro-layout'
dangerouslySetInnerHTML={{ __html: html }}
/>
</FullWidthRow>
<FullWidthRow>
<Link to={firstLessonPath}>
<Button block={true} bsSize='lg' bsStyle='primary'>
Go to the first lesson
</Button>
</Link>
<ButtonSpacer />
<hr />
</FullWidthRow>
<FullWidthRow>
<h2 className='intro-toc-title'>Upcoming Lessons</h2>
<ListGroup className='intro-toc'>
{renderMenuItems(allChallengeNode)}
</ListGroup>
</FullWidthRow>
</Fragment>
);
}
@ -22,9 +66,25 @@ IntroductionPage.propTypes = propTypes;
export default IntroductionPage;
export const query = graphql`
query IntroPageBySlug($slug: String!) {
query IntroPageBySlug($slug: String!, $block: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
frontmatter {
block
}
html
}
allChallengeNode(
filter: { block: { eq: $block } }
sort: { fields: [superOrder, order, suborder] }
) {
edges {
node {
fields {
slug
}
title
}
}
}
}
`;

View File

@ -0,0 +1,9 @@
.intro-toc .list-group-item:hover {
background-color: #eee;
}
.intro-toc a:hover {
text-decoration: none;
text-decoration-style: none;
color: #006400;
}

View File

@ -13,22 +13,16 @@ const preFormattedBlockNames = {
'the-dom': 'The DOM'
};
const noFormatting = [
'and',
'for',
'of',
'the',
'up',
'with'
];
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) => {
return phrase
.split('-')
.map(word => {
if (noFormatting.indexOf(word) !== -1) {
return word;
}

View File

@ -0,0 +1,85 @@
import { dasherize } from '..';
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 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: { block } } = edge.node;
createPage({
path: slug,
component: intro,
context: {
block: dasherize(block),
slug
}
});
};