From f64dbedfc61c786934ad147083ae2f14e7e2d323 Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Fri, 13 Apr 2018 15:33:03 +0100 Subject: [PATCH] Merge pull request #6 from Bouncey/feat/featureBuild Add project view --- packages/learn/package.json | 4 +- packages/learn/src/components/Header/index.js | 3 - .../components/formHelpers/BlockSaveButton.js | 18 ++ .../formHelpers/BlockSaveButton.test.js | 30 ++++ .../formHelpers/BlockSaveWrapper.js | 19 +++ .../formHelpers/BlockSaveWrapper.test.js | 16 ++ .../learn/src/components/formHelpers/Form.js | 80 +++++++++ .../src/components/formHelpers/Form.test.js | 43 +++++ .../src/components/formHelpers/FormFields.js | 85 ++++++++++ .../BlockSaveButton.test.js.snap | 11 ++ .../BlockSaveWrapper.test.js.snap | 11 ++ .../__snapshots__/Form.test.js.snap | 55 ++++++ .../learn/src/components/formHelpers/index.js | 73 ++++++++ .../learn/src/components/util/ButtonSpacer.js | 9 + .../src/components/util/ButtonSpacer.test.js | 16 ++ .../__snapshots__/ButtonSpacer.test.js.snap | 7 + packages/learn/src/layouts/index.js | 5 +- packages/learn/src/redux/app/sign-in-epic.js | 2 +- packages/learn/src/redux/store.js | 3 + .../Challenges/components/Solution-Input.jsx | 46 ----- .../templates/Challenges/project/Forms.jsx | 158 ------------------ .../templates/Challenges/project/Project.jsx | 58 ------- .../Challenges/project/ProjectForm.js | 68 ++++++++ .../src/templates/Challenges/project/Show.js | 94 +++++++---- .../project/{Side-Panel.jsx => Side-Panel.js} | 12 +- .../Challenges/project/Tool-Panel.js | 85 ++++++++++ .../Challenges/project/Tool-Panel.jsx | 147 ---------------- .../src/templates/Challenges/project/index.js | 1 - packages/learn/yarn.lock | 25 +++ 29 files changed, 726 insertions(+), 458 deletions(-) create mode 100644 packages/learn/src/components/formHelpers/BlockSaveButton.js create mode 100644 packages/learn/src/components/formHelpers/BlockSaveButton.test.js create mode 100644 packages/learn/src/components/formHelpers/BlockSaveWrapper.js create mode 100644 packages/learn/src/components/formHelpers/BlockSaveWrapper.test.js create mode 100644 packages/learn/src/components/formHelpers/Form.js create mode 100644 packages/learn/src/components/formHelpers/Form.test.js create mode 100644 packages/learn/src/components/formHelpers/FormFields.js create mode 100644 packages/learn/src/components/formHelpers/__snapshots__/BlockSaveButton.test.js.snap create mode 100644 packages/learn/src/components/formHelpers/__snapshots__/BlockSaveWrapper.test.js.snap create mode 100644 packages/learn/src/components/formHelpers/__snapshots__/Form.test.js.snap create mode 100644 packages/learn/src/components/formHelpers/index.js create mode 100644 packages/learn/src/components/util/ButtonSpacer.js create mode 100644 packages/learn/src/components/util/ButtonSpacer.test.js create mode 100644 packages/learn/src/components/util/__snapshots__/ButtonSpacer.test.js.snap delete mode 100644 packages/learn/src/templates/Challenges/components/Solution-Input.jsx delete mode 100644 packages/learn/src/templates/Challenges/project/Forms.jsx delete mode 100644 packages/learn/src/templates/Challenges/project/Project.jsx create mode 100644 packages/learn/src/templates/Challenges/project/ProjectForm.js rename packages/learn/src/templates/Challenges/project/{Side-Panel.jsx => Side-Panel.js} (72%) create mode 100644 packages/learn/src/templates/Challenges/project/Tool-Panel.js delete mode 100644 packages/learn/src/templates/Challenges/project/Tool-Panel.jsx delete mode 100644 packages/learn/src/templates/Challenges/project/index.js 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 ( +
+ + + {hideButton ? null : ( + errors[key]).length + } + > + {buttonText ? buttonText : null} + + )} + + + ); +} + +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 ( -
- { - isSubmitting ? - : - null - } - - - ); -} - -_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 ( -
- { - isSubmitting ? - : - null - } - { isSubmitting ? - - - : - null - } - - - ); -} - -_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"