committed by
Mrugesh Mohapatra
parent
ca6748a477
commit
f64dbedfc6
@ -39,10 +39,12 @@
|
|||||||
"react-test-renderer": "^16.3.1",
|
"react-test-renderer": "^16.3.1",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-actions": "^2.3.0",
|
"redux-actions": "^2.3.0",
|
||||||
|
"redux-form": "5",
|
||||||
"redux-observable": "^0.18.0",
|
"redux-observable": "^0.18.0",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^3.0.1",
|
||||||
"rxjs": "^5.5.7",
|
"rxjs": "^5.5.7",
|
||||||
"uglifyjs-webpack-plugin": "^1.2.4"
|
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||||
|
"validator": "^9.4.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"gatsby"
|
"gatsby"
|
||||||
|
@ -26,9 +26,6 @@ function Header() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link to='/sign-in'>
|
|
||||||
<button>Sign In</button>
|
|
||||||
</Link>
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
18
packages/learn/src/components/formHelpers/BlockSaveButton.js
Normal file
18
packages/learn/src/components/formHelpers/BlockSaveButton.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
function BlockSaveButton(props) {
|
||||||
|
return (
|
||||||
|
<Button block={true} bsSize='lg' bsStyle='primary' {...props} type='submit'>
|
||||||
|
{props.children || 'Save'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockSaveButton.displayName = 'BlockSaveButton';
|
||||||
|
BlockSaveButton.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockSaveButton;
|
@ -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('<BlockSaveButton /> snapshot', () => {
|
||||||
|
const component = renderer.create(<BlockSaveButton />);
|
||||||
|
let tree = component.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Button text should default to "Save"', () => {
|
||||||
|
const enzymeWrapper = Enzyme.render(<BlockSaveButton />);
|
||||||
|
|
||||||
|
expect(enzymeWrapper.text()).toBe('Save');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Button text should match "children"', () => {
|
||||||
|
const enzymeWrapper = Enzyme.render(
|
||||||
|
<BlockSaveButton>My Text Here</BlockSaveButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enzymeWrapper.text()).toBe('My Text Here');
|
||||||
|
});
|
@ -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 <div style={style}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockSaveWrapper.displayName = 'BlockSaveWrapper';
|
||||||
|
BlockSaveWrapper.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default BlockSaveWrapper;
|
@ -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('<BlockSaveWrapper /> snapshot', () => {
|
||||||
|
const component = renderer.create(<BlockSaveWrapper />);
|
||||||
|
let tree = component.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
80
packages/learn/src/components/formHelpers/Form.js
Normal file
80
packages/learn/src/components/formHelpers/Form.js
Normal file
@ -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 (
|
||||||
|
<form id={`dynamic-${id}`} onSubmit={handleSubmit(submit)}>
|
||||||
|
<FormFields errors={errors} fields={fields} options={options} />
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
{hideButton ? null : (
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={
|
||||||
|
(allPristine && !enableSubmit) ||
|
||||||
|
!!Object.keys(errors).filter(key => errors[key]).length
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{buttonText ? buttonText : null}
|
||||||
|
</BlockSaveButton>
|
||||||
|
)}
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicForm.displayName = 'DynamicForm';
|
||||||
|
DynamicForm.propTypes = propTypes;
|
||||||
|
|
||||||
|
const DynamicFormWithRedux = reduxForm()(DynamicForm);
|
||||||
|
|
||||||
|
export default function Form(props) {
|
||||||
|
return (
|
||||||
|
<DynamicFormWithRedux
|
||||||
|
{...props}
|
||||||
|
fields={props.formFields}
|
||||||
|
form={props.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Form.propTypes = propTypes;
|
43
packages/learn/src/components/formHelpers/Form.test.js
Normal file
43
packages/learn/src/components/formHelpers/Form.test.js
Normal file
@ -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('<DynamicForm /> snapshot', () => {
|
||||||
|
const component = renderer.create(<DynamicForm { ...defaultTestProps } />);
|
||||||
|
let tree = component.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
85
packages/learn/src/components/formHelpers/FormFields.js
Normal file
85
packages/learn/src/components/formHelpers/FormFields.js
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
{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 (
|
||||||
|
<Row className='inline-form-field' key={key}>
|
||||||
|
<Col sm={3} xs={12}>
|
||||||
|
{type === 'hidden' ? null : (
|
||||||
|
<ControlLabel htmlFor={key}>{_.startCase(name)}</ControlLabel>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col sm={9} xs={12}>
|
||||||
|
<FormControl
|
||||||
|
bsSize='lg'
|
||||||
|
componentClass={type === 'textarea' ? type : 'input'}
|
||||||
|
id={key}
|
||||||
|
name={name}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder ? name : ''}
|
||||||
|
required={required.includes(name)}
|
||||||
|
rows={4}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
{name in errors && !pristine ? (
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='danger'>{errors[name]}</Alert>
|
||||||
|
</HelpBlock>
|
||||||
|
) : null}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormFields.displayName = 'FormFields';
|
||||||
|
FormFields.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default FormFields;
|
@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<BlockSaveButton /> snapshot 1`] = `
|
||||||
|
<button
|
||||||
|
className="btn btn-lg btn-primary btn-block"
|
||||||
|
disabled={false}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
`;
|
@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<BlockSaveWrapper /> snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0 15px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
@ -0,0 +1,55 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<DynamicForm /> snapshot 1`] = `
|
||||||
|
<form
|
||||||
|
id="dynamic-my-test-form"
|
||||||
|
onSubmit={undefined}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="inline-form-field row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="col-sm-3 col-xs-12"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label"
|
||||||
|
htmlFor="name"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="col-sm-9 col-xs-12"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control input-lg"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="name"
|
||||||
|
required={false}
|
||||||
|
rows={4}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "0 15px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-lg btn-primary btn-block"
|
||||||
|
disabled={false}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
73
packages/learn/src/components/formHelpers/index.js
Normal file
73
packages/learn/src/components/formHelpers/index.js
Normal file
@ -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';
|
||||||
|
}
|
9
packages/learn/src/components/util/ButtonSpacer.js
Normal file
9
packages/learn/src/components/util/ButtonSpacer.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ButtonSpacer() {
|
||||||
|
return <div className='button-spacer' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonSpacer.displayName = 'ButtonSpacer';
|
||||||
|
|
||||||
|
export default ButtonSpacer;
|
16
packages/learn/src/components/util/ButtonSpacer.test.js
Normal file
16
packages/learn/src/components/util/ButtonSpacer.test.js
Normal 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 ButtonSpacer from './ButtonSpacer';
|
||||||
|
|
||||||
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
test('<ButtonSpacer /> snapshot', () => {
|
||||||
|
const component = renderer.create(<ButtonSpacer />);
|
||||||
|
let tree = component.toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ButtonSpacer /> snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="button-spacer"
|
||||||
|
/>
|
||||||
|
`;
|
@ -53,7 +53,10 @@ export default Layout;
|
|||||||
|
|
||||||
export const query = graphql`
|
export const query = graphql`
|
||||||
query LayoutQuery {
|
query LayoutQuery {
|
||||||
allChallengeNode(sort: { fields: [superOrder, order, suborder] }) {
|
allChallengeNode(
|
||||||
|
filter: { isPrivate: { eq: false } }
|
||||||
|
sort: { fields: [superOrder, order, suborder] }
|
||||||
|
) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
fields {
|
fields {
|
||||||
|
@ -13,7 +13,7 @@ export default function signInEpic(action$, _, { window }) {
|
|||||||
const request = {
|
const request = {
|
||||||
url: 'http://localhost:3000/passwordless-auth',
|
url: 'http://localhost:3000/passwordless-auth',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { email: payload, returnTo: window.location.origin }
|
body: { email: payload, return: window.location.origin }
|
||||||
};
|
};
|
||||||
|
|
||||||
return ajax(request).pipe(
|
return ajax(request).pipe(
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
import { combineEpics, createEpicMiddleware } from 'redux-observable';
|
import { combineEpics, createEpicMiddleware } from 'redux-observable';
|
||||||
import { routerReducer as router, routerMiddleware } from 'react-router-redux';
|
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 app, epics as appEpics } from './app';
|
||||||
import {
|
import {
|
||||||
reducer as challenge,
|
reducer as challenge,
|
||||||
@ -16,6 +18,7 @@ import { reducer as map } from '../components/Map/redux';
|
|||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
app,
|
app,
|
||||||
challenge,
|
challenge,
|
||||||
|
form: formReducer,
|
||||||
map,
|
map,
|
||||||
router
|
router
|
||||||
});
|
});
|
||||||
|
@ -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 (
|
|
||||||
<FormGroup
|
|
||||||
controlId='solution'
|
|
||||||
validationState={
|
|
||||||
(validationState && validationState.includes('warning')) ?
|
|
||||||
'warning' :
|
|
||||||
validationState
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl
|
|
||||||
name='solution'
|
|
||||||
placeholder={ placeholder }
|
|
||||||
type='url'
|
|
||||||
{ ...DOMOnlyProps(solution) }
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
validationState === 'error' &&
|
|
||||||
<HelpBlock>Make sure you provide a proper URL.</HelpBlock>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
validationState === 'glitch-warning' &&
|
|
||||||
<HelpBlock>
|
|
||||||
Make sure you have entered a shareable URL
|
|
||||||
(e.g. "https://green-camper.glitch.me", not
|
|
||||||
"https://glitch.com/#!/edit/green-camper".)
|
|
||||||
</HelpBlock>
|
|
||||||
}
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SolutionInput.displayName = 'SolutionInput';
|
|
||||||
SolutionInput.propTypes = propTypes;
|
|
@ -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 (
|
|
||||||
<form
|
|
||||||
name='NewFrontEndProject'
|
|
||||||
onSubmit={ handleSubmit(openChallengeModal) }
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isSubmitting ?
|
|
||||||
<SolutionInput
|
|
||||||
placeholder='https://codepen/your-project'
|
|
||||||
{ ...fields }
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-big'
|
|
||||||
onClick={ isSubmitting ? null : showProjectSubmit }
|
|
||||||
type={ isSubmitting ? 'submit' : null }
|
|
||||||
>
|
|
||||||
{ buttonCopy } (ctrl + enter)
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_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 (
|
|
||||||
<form
|
|
||||||
name='NewBackEndProject'
|
|
||||||
onSubmit={ handleSubmit(openChallengeModal) }
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isSubmitting ?
|
|
||||||
<SolutionInput
|
|
||||||
placeholder='https://your-app.com'
|
|
||||||
solution={ solution }
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{ isSubmitting ?
|
|
||||||
<FormGroup
|
|
||||||
controlId='githubLink'
|
|
||||||
validationState={ getValidationState(githubLink) }
|
|
||||||
>
|
|
||||||
<FormControl
|
|
||||||
name='githubLink'
|
|
||||||
placeholder='https://github.com/your-username/your-project'
|
|
||||||
type='url'
|
|
||||||
{ ...githubLink }
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-big'
|
|
||||||
onClick={ isSubmitting ? null : showProjectSubmit }
|
|
||||||
type={ isSubmitting ? 'submit' : null }
|
|
||||||
>
|
|
||||||
{ buttonCopy } (ctrl + enter)
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_BackEndForm.propTypes = propTypes;
|
|
||||||
|
|
||||||
export const BackEndForm = reduxForm(
|
|
||||||
{
|
|
||||||
form: 'NewBackEndProject',
|
|
||||||
fields: backEndFields,
|
|
||||||
validate: createFormValidator(backEndFieldValidators)
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
bindableActions
|
|
||||||
)(_BackEndForm);
|
|
@ -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 (
|
|
||||||
<Col
|
|
||||||
md={ 8 }
|
|
||||||
xs={ 12 }
|
|
||||||
>
|
|
||||||
<SidePanel
|
|
||||||
description={ description }
|
|
||||||
isCompleted={ isCompleted }
|
|
||||||
title={ title }
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<ToolPanel />
|
|
||||||
<HelpModal />
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Project.displayName = 'Project';
|
|
||||||
Project.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps
|
|
||||||
)(Project);
|
|
@ -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 (
|
||||||
|
<Form
|
||||||
|
buttonText={`${buttonCopy} (Ctrl + Enter)`}
|
||||||
|
formFields={isFrontEnd ? frontEndFields : backEndFields}
|
||||||
|
id={isFrontEnd ? 'front-end-form' : 'back-end-form'}
|
||||||
|
options={options}
|
||||||
|
submit={this.handleSubmit}
|
||||||
|
validate={createFormValidator(
|
||||||
|
isFrontEnd ? fieldValidators : backEndFieldValidators
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectForm.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm({
|
||||||
|
form: 'NewFrontEndProject',
|
||||||
|
fields: frontEndFields,
|
||||||
|
validate: createFormValidator(fieldValidators)
|
||||||
|
})(ProjectForm);
|
@ -1,40 +1,66 @@
|
|||||||
import React from 'react';
|
/* global graphql */
|
||||||
// import { addNS } from 'berkeleys-redux-utils';
|
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 Helmet from 'react-helmet';
|
||||||
// import Main from './Project.jsx';
|
|
||||||
// import ChildContainer from '../../Child-Container.jsx';
|
|
||||||
// import { types } from '../../redux';
|
|
||||||
// import Panes from '../../../../Panes';
|
|
||||||
// import _Map from '../../../../Map';
|
|
||||||
|
|
||||||
// const propTypes = {};
|
import { ChallengeNode } from '../../../redux/propTypes';
|
||||||
// export const mapStateToPanes = addNS(
|
import SidePanel from './Side-Panel';
|
||||||
// ns,
|
import ToolPanel from './Tool-Panel';
|
||||||
// () => ({
|
// import HelpModal from '../components/Help-Modal.jsx';
|
||||||
// [types.toggleMap]: 'Map',
|
|
||||||
// [types.toggleMain]: 'Main'
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const nameToComponent = {
|
const propTypes = {
|
||||||
// Map: _Map,
|
data: PropTypes.shape({
|
||||||
// Main: Main
|
challengeNode: ChallengeNode
|
||||||
// };
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// const renderPane = name => {
|
export class Project extends PureComponent {
|
||||||
// const Comp = nameToComponent[name];
|
render() {
|
||||||
// return Comp ? <Comp /> : <span>Pane { name } not found</span>;
|
const {
|
||||||
// };
|
data: {
|
||||||
|
challengeNode: {
|
||||||
export default function ShowProject() {
|
challengeType,
|
||||||
return (
|
fields: { blockName },
|
||||||
<h1>Project</h1>
|
title,
|
||||||
// <ChildContainer isFullWidth={ true }>
|
description,
|
||||||
// <Panes render={ renderPane }/>
|
guideUrl
|
||||||
// </ChildContainer>
|
}
|
||||||
);
|
}
|
||||||
|
} = this.props;
|
||||||
|
const blockNameTitle = `${blockName} - ${title}`;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Helmet title={`${blockNameTitle} | Learn freeCodeCamp}`} />
|
||||||
|
<SidePanel
|
||||||
|
className='full-height'
|
||||||
|
description={description}
|
||||||
|
guideUrl={guideUrl}
|
||||||
|
title={blockNameTitle}
|
||||||
|
/>
|
||||||
|
<ToolPanel challengeType={challengeType} helpChatRoom='help' />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowProject.displayName = 'ShowProject';
|
Project.displayName = 'Project';
|
||||||
// ShowProject.propTypes = propTypes;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ChallengeTitle from '../../Challenge-Title.jsx';
|
import ChallengeTitle from '../components/Challenge-Title';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
description: PropTypes.arrayOf(PropTypes.string),
|
description: PropTypes.arrayOf(PropTypes.string),
|
||||||
@ -15,7 +15,7 @@ export default class SidePanel extends PureComponent {
|
|||||||
<li
|
<li
|
||||||
className='step-text wrappable'
|
className='step-text wrappable'
|
||||||
dangerouslySetInnerHTML={{ __html: line }}
|
dangerouslySetInnerHTML={{ __html: line }}
|
||||||
key={ title.slice(6) + index }
|
key={title.slice(6) + index}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -24,12 +24,8 @@ export default class SidePanel extends PureComponent {
|
|||||||
const { title, description, isCompleted } = this.props;
|
const { title, description, isCompleted } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChallengeTitle isCompleted={ isCompleted }>
|
<ChallengeTitle isCompleted={isCompleted}>{title}</ChallengeTitle>
|
||||||
{ title }
|
<ul>{this.renderDescription(title, description)}</ul>
|
||||||
</ChallengeTitle>
|
|
||||||
<ul>
|
|
||||||
{ this.renderDescription(title, description) }
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
<ProjectForm isFrontEnd={isFrontEnd} />
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-primary-ghost btn-big'
|
||||||
|
componentClass='a'
|
||||||
|
href={`https://gitter.im/freecodecamp/${helpChatRoom}`}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
{guideUrl && (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-primary-ghost btn-big'
|
||||||
|
href={guideUrl}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
Get a hint
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-primary-ghost btn-big'
|
||||||
|
>
|
||||||
|
Ask for help on the forum
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolPanel.displayName = 'ProjectToolPanel';
|
||||||
|
ToolPanel.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ToolPanel;
|
@ -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 (
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-big'
|
|
||||||
onClick={ openChallengeModal }
|
|
||||||
>
|
|
||||||
{ buttonCopy } (ctrl + enter)
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
guideUrl,
|
|
||||||
helpChatRoom,
|
|
||||||
isFrontEnd,
|
|
||||||
isSimple,
|
|
||||||
isSignedIn,
|
|
||||||
isSubmitting,
|
|
||||||
openHelpModal,
|
|
||||||
openChallengeModal
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isSimple ?
|
|
||||||
this.renderSubmitButton(isSignedIn, openChallengeModal) :
|
|
||||||
<FormElement isSubmitting={ isSubmitting }/>
|
|
||||||
}
|
|
||||||
<ButtonSpacer />
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-primary-ghost btn-big'
|
|
||||||
componentClass='a'
|
|
||||||
href={ `https://gitter.im/freecodecamp/${helpChatRoom}` }
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Help
|
|
||||||
</Button>
|
|
||||||
<ButtonSpacer />
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-primary-ghost btn-big'
|
|
||||||
href={ guideUrl }
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Get a hint
|
|
||||||
</Button>
|
|
||||||
<ButtonSpacer />
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-primary-ghost btn-big'
|
|
||||||
onClick={ openHelpModal }
|
|
||||||
>
|
|
||||||
Ask for help on the forum
|
|
||||||
</Button>
|
|
||||||
<ButtonSpacer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolPanel.displayName = 'ProjectToolPanel';
|
|
||||||
ToolPanel.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ToolPanel);
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './Show.js';
|
|
@ -4809,6 +4809,10 @@ hoek@4.x.x:
|
|||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
|
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:
|
hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0:
|
||||||
version "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"
|
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"
|
version "16.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.1.tgz#ee66e6d8283224a83b3030e110056798488359ba"
|
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:
|
react-measure@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.0.2.tgz#072a9a5fafc01dfbadc1fa5fb09fc351037f636c"
|
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"
|
lodash "^4.2.0"
|
||||||
symbol-observable "^1.0.2"
|
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:
|
redux-observable@^0.18.0:
|
||||||
version "0.18.0"
|
version "0.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-0.18.0.tgz#48de1f35554b7ba23a88b18379ca1c93f5124197"
|
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-correct "^3.0.0"
|
||||||
spdx-expression-parse "^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:
|
value-equal@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
|
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
|
||||||
|
Reference in New Issue
Block a user