committed by
Mrugesh Mohapatra
parent
ca6748a477
commit
f64dbedfc6
@ -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"
|
||||
|
@ -26,9 +26,6 @@ function Header() {
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to='/sign-in'>
|
||||
<button>Sign In</button>
|
||||
</Link>
|
||||
</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`
|
||||
query LayoutQuery {
|
||||
allChallengeNode(sort: { fields: [superOrder, order, suborder] }) {
|
||||
allChallengeNode(
|
||||
filter: { isPrivate: { eq: false } }
|
||||
sort: { fields: [superOrder, order, suborder] }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
fields {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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';
|
||||
// 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 ? <Comp /> : <span>Pane { name } not found</span>;
|
||||
// };
|
||||
|
||||
export default function ShowProject() {
|
||||
return (
|
||||
<h1>Project</h1>
|
||||
// <ChildContainer isFullWidth={ true }>
|
||||
// <Panes render={ renderPane }/>
|
||||
// </ChildContainer>
|
||||
);
|
||||
export class Project extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challengeType,
|
||||
fields: { blockName },
|
||||
title,
|
||||
description,
|
||||
guideUrl
|
||||
}
|
||||
}
|
||||
} = 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';
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -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 {
|
||||
<li
|
||||
className='step-text wrappable'
|
||||
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;
|
||||
return (
|
||||
<div>
|
||||
<ChallengeTitle isCompleted={ isCompleted }>
|
||||
{ title }
|
||||
</ChallengeTitle>
|
||||
<ul>
|
||||
{ this.renderDescription(title, description) }
|
||||
</ul>
|
||||
<ChallengeTitle isCompleted={isCompleted}>{title}</ChallengeTitle>
|
||||
<ul>{this.renderDescription(title, description)}</ul>
|
||||
</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"
|
||||
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"
|
||||
|
Reference in New Issue
Block a user