diff --git a/packages/learn/package.json b/packages/learn/package.json
index f3118fc960..2dec7ac292 100644
--- a/packages/learn/package.json
+++ b/packages/learn/package.json
@@ -39,10 +39,12 @@
"react-test-renderer": "^16.3.1",
"redux": "^3.7.2",
"redux-actions": "^2.3.0",
+ "redux-form": "5",
"redux-observable": "^0.18.0",
"reselect": "^3.0.1",
"rxjs": "^5.5.7",
- "uglifyjs-webpack-plugin": "^1.2.4"
+ "uglifyjs-webpack-plugin": "^1.2.4",
+ "validator": "^9.4.1"
},
"keywords": [
"gatsby"
diff --git a/packages/learn/src/components/Header/index.js b/packages/learn/src/components/Header/index.js
index 2fbf03d13e..1483f921da 100644
--- a/packages/learn/src/components/Header/index.js
+++ b/packages/learn/src/components/Header/index.js
@@ -26,9 +26,6 @@ function Header() {
/>
-
-
-
);
}
diff --git a/packages/learn/src/components/formHelpers/BlockSaveButton.js b/packages/learn/src/components/formHelpers/BlockSaveButton.js
new file mode 100644
index 0000000000..d3fb19de91
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/BlockSaveButton.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button } from 'react-bootstrap';
+
+function BlockSaveButton(props) {
+ return (
+
+ );
+}
+
+BlockSaveButton.displayName = 'BlockSaveButton';
+BlockSaveButton.propTypes = {
+ children: PropTypes.any
+};
+
+export default BlockSaveButton;
diff --git a/packages/learn/src/components/formHelpers/BlockSaveButton.test.js b/packages/learn/src/components/formHelpers/BlockSaveButton.test.js
new file mode 100644
index 0000000000..a15c60d105
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/BlockSaveButton.test.js
@@ -0,0 +1,30 @@
+/* global expect */
+
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+import BlockSaveButton from './BlockSaveButton';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+test(' snapshot', () => {
+ const component = renderer.create();
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+test('Button text should default to "Save"', () => {
+ const enzymeWrapper = Enzyme.render();
+
+ expect(enzymeWrapper.text()).toBe('Save');
+});
+
+test('Button text should match "children"', () => {
+ const enzymeWrapper = Enzyme.render(
+ My Text Here
+ );
+
+ expect(enzymeWrapper.text()).toBe('My Text Here');
+});
diff --git a/packages/learn/src/components/formHelpers/BlockSaveWrapper.js b/packages/learn/src/components/formHelpers/BlockSaveWrapper.js
new file mode 100644
index 0000000000..9e977789b6
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/BlockSaveWrapper.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ children: PropTypes.node
+};
+
+const style = {
+ padding: '0 15px'
+};
+
+function BlockSaveWrapper({ children }) {
+ return
{children}
;
+}
+
+BlockSaveWrapper.displayName = 'BlockSaveWrapper';
+BlockSaveWrapper.propTypes = propTypes;
+
+export default BlockSaveWrapper;
diff --git a/packages/learn/src/components/formHelpers/BlockSaveWrapper.test.js b/packages/learn/src/components/formHelpers/BlockSaveWrapper.test.js
new file mode 100644
index 0000000000..535ddca0b5
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/BlockSaveWrapper.test.js
@@ -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 BlockSaveWrapper from './BlockSaveWrapper';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+test(' snapshot', () => {
+ const component = renderer.create();
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/packages/learn/src/components/formHelpers/Form.js b/packages/learn/src/components/formHelpers/Form.js
new file mode 100644
index 0000000000..a1ddc8055f
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/Form.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { reduxForm } from 'redux-form';
+
+import { FormFields, BlockSaveButton, BlockSaveWrapper } from './';
+
+const propTypes = {
+ buttonText: PropTypes.string,
+ enableSubmit: PropTypes.bool,
+ errors: PropTypes.object,
+ fields: PropTypes.objectOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.string.isRequired
+ })
+ ),
+ formFields: PropTypes.arrayOf(PropTypes.string).isRequired,
+ handleSubmit: PropTypes.func,
+ hideButton: PropTypes.bool,
+ id: PropTypes.string.isRequired,
+ initialValues: PropTypes.object,
+ options: PropTypes.shape({
+ ignored: PropTypes.arrayOf(PropTypes.string),
+ required: PropTypes.arrayOf(PropTypes.string),
+ types: PropTypes.objectOf(PropTypes.string)
+ }),
+ submit: PropTypes.func.isRequired
+};
+
+export function DynamicForm({
+ // redux-form
+ errors,
+ fields,
+ handleSubmit,
+ fields: { _meta: { allPristine } },
+
+ // HOC
+ buttonText,
+ enableSubmit,
+ hideButton,
+ id,
+ options,
+ submit
+}) {
+ return (
+
+ );
+}
+
+DynamicForm.displayName = 'DynamicForm';
+DynamicForm.propTypes = propTypes;
+
+const DynamicFormWithRedux = reduxForm()(DynamicForm);
+
+export default function Form(props) {
+ return (
+
+ );
+}
+
+Form.propTypes = propTypes;
diff --git a/packages/learn/src/components/formHelpers/Form.test.js b/packages/learn/src/components/formHelpers/Form.test.js
new file mode 100644
index 0000000000..7f1add6ca3
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/Form.test.js
@@ -0,0 +1,43 @@
+/* global expect */
+
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+import { DynamicForm } from './Form';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+const defaultTestProps = {
+ errors: {},
+ fields: {
+ _meta: {
+ allPristine: true,
+ name: 'name',
+ onChange: () => {},
+ value: ''
+ }
+ },
+ handleSubmit: () => {},
+
+ buttonText: 'Submit',
+ enableSubmit: true,
+ formFields: ['name', 'website'],
+ hideButton: false,
+ id: 'my-test-form',
+ options: {
+ types: {
+ name: 'text',
+ website: 'url'
+ },
+ required: ['website']
+ },
+ submit: () => {}
+};
+
+test(' snapshot', () => {
+ const component = renderer.create();
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/packages/learn/src/components/formHelpers/FormFields.js b/packages/learn/src/components/formHelpers/FormFields.js
new file mode 100644
index 0000000000..9372a5c6df
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/FormFields.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+import {
+ Alert,
+ Col,
+ ControlLabel,
+ FormControl,
+ HelpBlock,
+ Row
+} from 'react-bootstrap';
+
+const propTypes = {
+ errors: PropTypes.objectOf(PropTypes.string),
+ fields: PropTypes.objectOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ options: PropTypes.shape({
+ errors: PropTypes.objectOf(
+ PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(null)])
+ ),
+ ignored: PropTypes.arrayOf(PropTypes.string),
+ placeholder: PropTypes.bool,
+ required: PropTypes.arrayOf(PropTypes.string),
+ types: PropTypes.objectOf(PropTypes.string)
+ })
+};
+
+function FormFields(props) {
+ const { errors = {}, fields, options = {} } = props;
+ const {
+ ignored = [],
+ placeholder = true,
+ required = [],
+ types = {}
+ } = options;
+ return (
+
+ {Object.keys(fields)
+ .filter(field => !ignored.includes(field))
+ .map(key => fields[key])
+ .map(({ name, onChange, value, pristine }) => {
+ const key = _.kebabCase(name);
+ const type = name in types ? types[name] : 'text';
+ return (
+
+
+ {type === 'hidden' ? null : (
+ {_.startCase(name)}
+ )}
+
+
+
+ {name in errors && !pristine ? (
+
+ {errors[name]}
+
+ ) : null}
+
+
+ );
+ })}
+
+ );
+}
+
+FormFields.displayName = 'FormFields';
+FormFields.propTypes = propTypes;
+
+export default FormFields;
diff --git a/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveButton.test.js.snap b/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveButton.test.js.snap
new file mode 100644
index 0000000000..ac1e9f95dd
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveButton.test.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` snapshot 1`] = `
+
+`;
diff --git a/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveWrapper.test.js.snap b/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveWrapper.test.js.snap
new file mode 100644
index 0000000000..5cd8a7cdf7
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/__snapshots__/BlockSaveWrapper.test.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` snapshot 1`] = `
+
+`;
diff --git a/packages/learn/src/components/formHelpers/__snapshots__/Form.test.js.snap b/packages/learn/src/components/formHelpers/__snapshots__/Form.test.js.snap
new file mode 100644
index 0000000000..c2f6aa65da
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/__snapshots__/Form.test.js.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` snapshot 1`] = `
+
+`;
diff --git a/packages/learn/src/components/formHelpers/index.js b/packages/learn/src/components/formHelpers/index.js
new file mode 100644
index 0000000000..ec9c9e182c
--- /dev/null
+++ b/packages/learn/src/components/formHelpers/index.js
@@ -0,0 +1,73 @@
+import normalizeUrl from 'normalize-url';
+import isURL from 'validator/lib/isURL';
+
+export { default as BlockSaveButton } from './BlockSaveButton.js';
+export { default as BlockSaveWrapper } from './BlockSaveWrapper.js';
+export { default as Form } from './Form.js';
+export { default as FormFields } from './FormFields.js';
+
+const normalizeOptions = {
+ stripWWW: false
+};
+
+// callIfDefined(fn: (Any) => Any) => (value: Any) => Any
+export function callIfDefined(fn) {
+ return value => (value ? fn(value) : value);
+}
+
+// formatUrl(url: String) => String
+export function formatUrl(url) {
+ if (typeof url === 'string' && url.length > 4 && url.indexOf('.') !== -1) {
+ // prevent trailing / from being stripped during typing
+ let lastChar = '';
+ if (url.substring(url.length - 1) === '/') {
+ lastChar = '/';
+ }
+ // prevent normalize-url from stripping last dot during typing
+ if (url.substring(url.length - 1) === '.') {
+ lastChar = '.';
+ }
+ return normalizeUrl(url, normalizeOptions) + lastChar;
+ }
+ return url;
+}
+
+export function isValidURL(data) {
+ /* eslint-disable camelcase */
+ return isURL(data, { require_protocol: true });
+ /* eslint-enable camelcase */
+}
+
+export function makeOptional(validator) {
+ return val => (val ? validator(val) : true);
+}
+
+export function makeRequired(validator) {
+ return val => (val ? validator(val) : false);
+}
+
+export function createFormValidator(fieldValidators) {
+ const fieldKeys = Object.keys(fieldValidators);
+ return values =>
+ fieldKeys
+ .map(field => {
+ if (fieldValidators[field](values[field])) {
+ return null;
+ }
+ return { [field]: !fieldValidators[field](values[field]) };
+ })
+ .filter(Boolean)
+ .reduce((errors, error) => ({ ...errors, ...error }), {});
+}
+
+export function getValidationState(field) {
+ if (field.pristine) {
+ return null;
+ }
+
+ if (/https?:\/\/glitch\.com\/edit\/#!\/.*/g.test(field.value)) {
+ return 'glitch-warning';
+ }
+
+ return field.error ? 'error' : 'success';
+}
diff --git a/packages/learn/src/components/util/ButtonSpacer.js b/packages/learn/src/components/util/ButtonSpacer.js
new file mode 100644
index 0000000000..3e46a8654a
--- /dev/null
+++ b/packages/learn/src/components/util/ButtonSpacer.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+function ButtonSpacer() {
+ return ;
+}
+
+ButtonSpacer.displayName = 'ButtonSpacer';
+
+export default ButtonSpacer;
diff --git a/packages/learn/src/components/util/ButtonSpacer.test.js b/packages/learn/src/components/util/ButtonSpacer.test.js
new file mode 100644
index 0000000000..78fb12c116
--- /dev/null
+++ b/packages/learn/src/components/util/ButtonSpacer.test.js
@@ -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 ButtonSpacer from './ButtonSpacer';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+test(' snapshot', () => {
+ const component = renderer.create();
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/packages/learn/src/components/util/__snapshots__/ButtonSpacer.test.js.snap b/packages/learn/src/components/util/__snapshots__/ButtonSpacer.test.js.snap
new file mode 100644
index 0000000000..b9c0bb7c0a
--- /dev/null
+++ b/packages/learn/src/components/util/__snapshots__/ButtonSpacer.test.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` snapshot 1`] = `
+
+`;
diff --git a/packages/learn/src/layouts/index.js b/packages/learn/src/layouts/index.js
index d4f5669595..6634665fe1 100644
--- a/packages/learn/src/layouts/index.js
+++ b/packages/learn/src/layouts/index.js
@@ -53,7 +53,10 @@ export default Layout;
export const query = graphql`
query LayoutQuery {
- allChallengeNode(sort: { fields: [superOrder, order, suborder] }) {
+ allChallengeNode(
+ filter: { isPrivate: { eq: false } }
+ sort: { fields: [superOrder, order, suborder] }
+ ) {
edges {
node {
fields {
diff --git a/packages/learn/src/redux/app/sign-in-epic.js b/packages/learn/src/redux/app/sign-in-epic.js
index f6eb121f48..e6df965747 100644
--- a/packages/learn/src/redux/app/sign-in-epic.js
+++ b/packages/learn/src/redux/app/sign-in-epic.js
@@ -13,7 +13,7 @@ export default function signInEpic(action$, _, { window }) {
const request = {
url: 'http://localhost:3000/passwordless-auth',
method: 'POST',
- body: { email: payload, returnTo: window.location.origin }
+ body: { email: payload, return: window.location.origin }
};
return ajax(request).pipe(
diff --git a/packages/learn/src/redux/store.js b/packages/learn/src/redux/store.js
index cd50379a5b..ef826221c1 100644
--- a/packages/learn/src/redux/store.js
+++ b/packages/learn/src/redux/store.js
@@ -6,6 +6,8 @@ import {
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import { routerReducer as router, routerMiddleware } from 'react-router-redux';
+import { reducer as formReducer } from 'redux-form';
+
import { reducer as app, epics as appEpics } from './app';
import {
reducer as challenge,
@@ -16,6 +18,7 @@ import { reducer as map } from '../components/Map/redux';
const rootReducer = combineReducers({
app,
challenge,
+ form: formReducer,
map,
router
});
diff --git a/packages/learn/src/templates/Challenges/components/Solution-Input.jsx b/packages/learn/src/templates/Challenges/components/Solution-Input.jsx
deleted file mode 100644
index c6be18f8dc..0000000000
--- a/packages/learn/src/templates/Challenges/components/Solution-Input.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { HelpBlock, FormGroup, FormControl } from 'react-bootstrap';
-import { getValidationState, DOMOnlyProps } from '../../utils/form';
-
-const propTypes = {
- placeholder: PropTypes.string,
- solution: PropTypes.object
-};
-
-export default function SolutionInput({ solution, placeholder }) {
- const validationState = getValidationState(solution);
-
- return (
-
-
- {
- validationState === 'error' &&
- Make sure you provide a proper URL.
- }
- {
- validationState === 'glitch-warning' &&
-
- Make sure you have entered a shareable URL
- (e.g. "https://green-camper.glitch.me", not
- "https://glitch.com/#!/edit/green-camper".)
-
- }
-
- );
-}
-
-SolutionInput.displayName = 'SolutionInput';
-SolutionInput.propTypes = propTypes;
diff --git a/packages/learn/src/templates/Challenges/project/Forms.jsx b/packages/learn/src/templates/Challenges/project/Forms.jsx
deleted file mode 100644
index b563412dc6..0000000000
--- a/packages/learn/src/templates/Challenges/project/Forms.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { reduxForm } from 'redux-form';
-import {
- Button,
- FormGroup,
- FormControl
-} from 'react-bootstrap';
-
-import { showProjectSubmit } from './redux';
-import SolutionInput from '../../Solution-Input.jsx';
-import { openChallengeModal } from '../../redux';
-import {
- isValidURL,
- makeRequired,
- createFormValidator,
- getValidationState
-} from '../../../../utils/form';
-
-const propTypes = {
- fields: PropTypes.object,
- handleSubmit: PropTypes.func,
- isSignedIn: PropTypes.bool,
- isSubmitting: PropTypes.bool,
- openChallengeModal: PropTypes.func.isRequired,
- resetForm: PropTypes.func,
- showProjectSubmit: PropTypes.func,
- submitChallenge: PropTypes.func
-};
-
-const bindableActions = {
- openChallengeModal,
- showProjectSubmit
-};
-const frontEndFields = [ 'solution' ];
-const backEndFields = [
- 'solution',
- 'githubLink'
-];
-
-const fieldValidators = {
- solution: makeRequired(isValidURL)
-};
-
-const backEndFieldValidators = {
- ...fieldValidators,
- githubLink: makeRequired(isValidURL)
-};
-
-export function _FrontEndForm({
- fields,
- handleSubmit,
- openChallengeModal,
- isSubmitting,
- showProjectSubmit
-}) {
- const buttonCopy = isSubmitting ?
- 'Submit and go to my next challenge' :
- "I've completed this challenge";
- return (
-
- );
-}
-
-_FrontEndForm.propTypes = propTypes;
-
-export const FrontEndForm = reduxForm(
- {
- form: 'NewFrontEndProject',
- fields: frontEndFields,
- validate: createFormValidator(fieldValidators)
- },
- null,
- bindableActions
-)(_FrontEndForm);
-
-export function _BackEndForm({
- fields: { solution, githubLink },
- handleSubmit,
- openChallengeModal,
- isSubmitting,
- showProjectSubmit
-}) {
- const buttonCopy = isSubmitting ?
- 'Submit and go to my next challenge' :
- "I've completed this challenge";
- return (
-
- );
-}
-
-_BackEndForm.propTypes = propTypes;
-
-export const BackEndForm = reduxForm(
- {
- form: 'NewBackEndProject',
- fields: backEndFields,
- validate: createFormValidator(backEndFieldValidators)
- },
- null,
- bindableActions
-)(_BackEndForm);
diff --git a/packages/learn/src/templates/Challenges/project/Project.jsx b/packages/learn/src/templates/Challenges/project/Project.jsx
deleted file mode 100644
index 977cfa2aca..0000000000
--- a/packages/learn/src/templates/Challenges/project/Project.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { createSelector } from 'reselect';
-import { connect } from 'react-redux';
-import { Col } from 'react-bootstrap';
-
-import SidePanel from './Side-Panel.jsx';
-import ToolPanel from './Tool-Panel.jsx';
-import HelpModal from '../../Help-Modal.jsx';
-
-import { challengeMetaSelector } from '../../redux';
-import { challengeSelector } from '../../../../redux';
-
-const mapStateToProps = createSelector(
- challengeSelector,
- challengeMetaSelector,
- ({ description }, { title }) => ({
- title,
- description
- })
-);
-const propTypes = {
- description: PropTypes.arrayOf(PropTypes.string),
- isCompleted: PropTypes.bool,
- title: PropTypes.string
-};
-
-export class Project extends PureComponent {
- render() {
- const {
- title,
- isCompleted,
- description
- } = this.props;
- return (
-
-
-
-
-
-
- );
- }
-}
-
-Project.displayName = 'Project';
-Project.propTypes = propTypes;
-
-export default connect(
- mapStateToProps
-)(Project);
diff --git a/packages/learn/src/templates/Challenges/project/ProjectForm.js b/packages/learn/src/templates/Challenges/project/ProjectForm.js
new file mode 100644
index 0000000000..ea9ae0b642
--- /dev/null
+++ b/packages/learn/src/templates/Challenges/project/ProjectForm.js
@@ -0,0 +1,68 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { reduxForm } from 'redux-form';
+
+import {
+ Form,
+ isValidURL,
+ makeRequired,
+ createFormValidator
+} from '../../../components/formHelpers';
+
+const propTypes = {
+ isFrontEnd: PropTypes.bool,
+ isSubmitting: PropTypes.bool
+};
+
+const frontEndFields = ['solution'];
+const backEndFields = ['solution', 'githubLink'];
+
+const fieldValidators = {
+ solution: makeRequired(isValidURL)
+};
+
+const backEndFieldValidators = {
+ ...fieldValidators,
+ githubLink: makeRequired(isValidURL)
+};
+
+const options = {
+ types: {
+ solution: 'url',
+ githubLink: 'url'
+ },
+ required: ['solution', 'githubLink']
+};
+
+export class ProjectForm extends PureComponent {
+ handleSubmit = values => {
+ console.log(values);
+ };
+
+ render() {
+ const { isSubmitting, isFrontEnd } = this.props;
+ const buttonCopy = isSubmitting
+ ? 'Submit and go to my next challenge'
+ : "I've completed this challenge";
+ return (
+
+ );
+ }
+}
+
+ProjectForm.propTypes = propTypes;
+
+export default reduxForm({
+ form: 'NewFrontEndProject',
+ fields: frontEndFields,
+ validate: createFormValidator(fieldValidators)
+})(ProjectForm);
diff --git a/packages/learn/src/templates/Challenges/project/Show.js b/packages/learn/src/templates/Challenges/project/Show.js
index 07b1e95e88..c1b88789f0 100644
--- a/packages/learn/src/templates/Challenges/project/Show.js
+++ b/packages/learn/src/templates/Challenges/project/Show.js
@@ -1,40 +1,66 @@
-import React from 'react';
-// import { addNS } from 'berkeleys-redux-utils';
+/* global graphql */
+import React, { PureComponent, Fragment } from 'react';
+import PropTypes from 'prop-types';
+// import { createSelector } from 'reselect';
+// import { connect } from 'react-redux';
-// import ns from './ns.json';
-// import Main from './Project.jsx';
-// import ChildContainer from '../../Child-Container.jsx';
-// import { types } from '../../redux';
-// import Panes from '../../../../Panes';
-// import _Map from '../../../../Map';
+import Helmet from 'react-helmet';
-// const propTypes = {};
-// export const mapStateToPanes = addNS(
-// ns,
-// () => ({
-// [types.toggleMap]: 'Map',
-// [types.toggleMain]: 'Main'
-// })
-// );
+import { ChallengeNode } from '../../../redux/propTypes';
+import SidePanel from './Side-Panel';
+import ToolPanel from './Tool-Panel';
+// import HelpModal from '../components/Help-Modal.jsx';
-// const nameToComponent = {
-// Map: _Map,
-// Main: Main
-// };
+const propTypes = {
+ data: PropTypes.shape({
+ challengeNode: ChallengeNode
+ })
+};
-// const renderPane = name => {
-// const Comp = nameToComponent[name];
-// return Comp ? : Pane { name } not found;
-// };
-
-export default function ShowProject() {
- return (
- Project
- //
- //
- //
- );
+export class Project extends PureComponent {
+ render() {
+ const {
+ data: {
+ challengeNode: {
+ challengeType,
+ fields: { blockName },
+ title,
+ description,
+ guideUrl
+ }
+ }
+ } = this.props;
+ const blockNameTitle = `${blockName} - ${title}`;
+ return (
+
+
+
+
+
+ );
+ }
}
-ShowProject.displayName = 'ShowProject';
-// ShowProject.propTypes = propTypes;
+Project.displayName = 'Project';
+Project.propTypes = propTypes;
+
+export default Project;
+
+export const query = graphql`
+ query ProjectChallenge($slug: String!) {
+ challengeNode(fields: { slug: { eq: $slug } }) {
+ title
+ guideUrl
+ description
+ challengeType
+ fields {
+ blockName
+ }
+ }
+ }
+`;
diff --git a/packages/learn/src/templates/Challenges/project/Side-Panel.jsx b/packages/learn/src/templates/Challenges/project/Side-Panel.js
similarity index 72%
rename from packages/learn/src/templates/Challenges/project/Side-Panel.jsx
rename to packages/learn/src/templates/Challenges/project/Side-Panel.js
index b93a3a7b3f..959f4b0a29 100644
--- a/packages/learn/src/templates/Challenges/project/Side-Panel.jsx
+++ b/packages/learn/src/templates/Challenges/project/Side-Panel.js
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import ChallengeTitle from '../../Challenge-Title.jsx';
+import ChallengeTitle from '../components/Challenge-Title';
const propTypes = {
description: PropTypes.arrayOf(PropTypes.string),
@@ -15,7 +15,7 @@ export default class SidePanel extends PureComponent {
));
}
@@ -24,12 +24,8 @@ export default class SidePanel extends PureComponent {
const { title, description, isCompleted } = this.props;
return (
-
- { title }
-
-
- { this.renderDescription(title, description) }
-
+
{title}
+
{this.renderDescription(title, description)}
);
}
diff --git a/packages/learn/src/templates/Challenges/project/Tool-Panel.js b/packages/learn/src/templates/Challenges/project/Tool-Panel.js
new file mode 100644
index 0000000000..978642cd91
--- /dev/null
+++ b/packages/learn/src/templates/Challenges/project/Tool-Panel.js
@@ -0,0 +1,85 @@
+import React, { PureComponent, Fragment } from 'react';
+import PropTypes from 'prop-types';
+// import { connect } from 'react-redux';
+// import { createSelector } from 'reselect';
+import { Button } from 'react-bootstrap';
+
+import ButtonSpacer from '../../../components/util/ButtonSpacer';
+import ProjectForm from './ProjectForm';
+
+// import { submittingSelector } from './redux';
+
+// import {
+// openChallengeModal,
+
+// openHelpModal,
+// chatRoomSelector,
+// guideURLSelector
+// } from '../../redux';
+
+// import {
+// signInLoadingSelector,
+// challengeSelector
+// } from '../../../../redux';
+import { challengeTypes } from '../../../../utils/challengeTypes';
+
+const { frontEndProject } = challengeTypes;
+
+const propTypes = {
+ challengeType: PropTypes.number,
+ guideUrl: PropTypes.string,
+ helpChatRoom: PropTypes.string.isRequired
+};
+
+export class ToolPanel extends PureComponent {
+ render() {
+ const { guideUrl, helpChatRoom, challengeType } = this.props;
+ console.log(challengeType, frontEndProject);
+
+ const isFrontEnd = challengeType === frontEndProject;
+ return (
+
+
+
+
+
+ {guideUrl && (
+
+
+
+
+ )}
+
+
+
+ );
+ }
+}
+
+ToolPanel.displayName = 'ProjectToolPanel';
+ToolPanel.propTypes = propTypes;
+
+export default ToolPanel;
diff --git a/packages/learn/src/templates/Challenges/project/Tool-Panel.jsx b/packages/learn/src/templates/Challenges/project/Tool-Panel.jsx
deleted file mode 100644
index a7c3e4ad1d..0000000000
--- a/packages/learn/src/templates/Challenges/project/Tool-Panel.jsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { Button } from 'react-bootstrap';
-
-import { ButtonSpacer } from '../../../../helperComponents';
-import {
- FrontEndForm,
- BackEndForm
-} from './Forms.jsx';
-
-import { submittingSelector } from './redux';
-
-import {
- openChallengeModal,
-
- openHelpModal,
- chatRoomSelector,
- guideURLSelector
-} from '../../redux';
-
-import {
- signInLoadingSelector,
- challengeSelector
-} from '../../../../redux';
-import {
- simpleProject,
- frontEndProject
-} from '../../../../utils/challengeTypes';
-
-const propTypes = {
- guideUrl: PropTypes.string,
- helpChatRoom: PropTypes.string.isRequired,
- isFrontEnd: PropTypes.bool,
- isSignedIn: PropTypes.bool,
- isSimple: PropTypes.bool,
- isSubmitting: PropTypes.bool,
- openChallengeModal: PropTypes.func.isRequired,
- openHelpModal: PropTypes.func.isRequired
-};
-const mapDispatchToProps = {
- openChallengeModal,
- openHelpModal
-};
-const mapStateToProps = createSelector(
- challengeSelector,
- signInLoadingSelector,
- submittingSelector,
- chatRoomSelector,
- guideURLSelector,
- (
- { challengeType = simpleProject },
- showLoading,
- isSubmitting,
- helpChatRoom,
- guideUrl
- ) => ({
- guideUrl,
- helpChatRoom,
- isSignedIn: !showLoading,
- isSubmitting,
- isSimple: challengeType === simpleProject,
- isFrontEnd: challengeType === frontEndProject
- })
-);
-
-export class ToolPanel extends PureComponent {
- renderSubmitButton(isSignedIn, openChallengeModal) {
- const buttonCopy = isSignedIn ?
- 'Submit and go to my next challenge' :
- "I've completed this challenge";
- return (
-
- );
- }
-
- render() {
- const {
- guideUrl,
- helpChatRoom,
- isFrontEnd,
- isSimple,
- isSignedIn,
- isSubmitting,
- openHelpModal,
- openChallengeModal
- } = this.props;
-
- const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
- return (
-
- {
- isSimple ?
- this.renderSubmitButton(isSignedIn, openChallengeModal) :
-
- }
-
-
-
-
-
-
-
-
- );
- }
-}
-
-ToolPanel.displayName = 'ProjectToolPanel';
-ToolPanel.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ToolPanel);
diff --git a/packages/learn/src/templates/Challenges/project/index.js b/packages/learn/src/templates/Challenges/project/index.js
deleted file mode 100644
index 238f9c2da1..0000000000
--- a/packages/learn/src/templates/Challenges/project/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './Show.js';
diff --git a/packages/learn/yarn.lock b/packages/learn/yarn.lock
index 1dc0c551ab..bdfdf6412b 100644
--- a/packages/learn/yarn.lock
+++ b/packages/learn/yarn.lock
@@ -4809,6 +4809,10 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
+hoist-non-react-statics@^1.0.5:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
+
hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
@@ -8421,6 +8425,12 @@ react-is@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.1.tgz#ee66e6d8283224a83b3030e110056798488359ba"
+react-lazy-cache@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/react-lazy-cache/-/react-lazy-cache-3.0.1.tgz#0dc64d38df1767ef77678c5c94190064cb11b0cd"
+ dependencies:
+ deep-equal "^1.0.1"
+
react-measure@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.0.2.tgz#072a9a5fafc01dfbadc1fa5fb09fc351037f636c"
@@ -8727,6 +8737,17 @@ redux-devtools-instrument@^1.3.3:
lodash "^4.2.0"
symbol-observable "^1.0.2"
+redux-form@5:
+ version "5.3.6"
+ resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-5.3.6.tgz#f77a81dbf38d44d26ea411100a23f19e29cd1946"
+ dependencies:
+ deep-equal "^1.0.1"
+ hoist-non-react-statics "^1.0.5"
+ invariant "^2.0.0"
+ is-promise "^2.1.0"
+ prop-types "^15.5.8"
+ react-lazy-cache "^3.0.1"
+
redux-observable@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-0.18.0.tgz#48de1f35554b7ba23a88b18379ca1c93f5124197"
@@ -10729,6 +10750,10 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+validator@^9.4.1:
+ version "9.4.1"
+ resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
+
value-equal@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"