Merge pull request #6 from Bouncey/feat/featureBuild

Add project view
This commit is contained in:
Stuart Taylor
2018-04-13 15:33:03 +01:00
committed by Mrugesh Mohapatra
parent ca6748a477
commit f64dbedfc6
29 changed files with 726 additions and 458 deletions

View File

@ -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"

View File

@ -26,9 +26,6 @@ function Header() {
/>
</Link>
</div>
<Link to='/sign-in'>
<button>Sign In</button>
</Link>
</header>
);
}

View 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;

View File

@ -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');
});

View File

@ -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;

View File

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

View 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;

View 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();
});

View 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;

View File

@ -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>
`;

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BlockSaveWrapper /> snapshot 1`] = `
<div
style={
Object {
"padding": "0 15px",
}
}
/>
`;

View File

@ -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>
`;

View 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';
}

View File

@ -0,0 +1,9 @@
import React from 'react';
function ButtonSpacer() {
return <div className='button-spacer' />;
}
ButtonSpacer.displayName = 'ButtonSpacer';
export default ButtonSpacer;

View File

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

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ButtonSpacer /> snapshot 1`] = `
<div
className="button-spacer"
/>
`;

View File

@ -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 {

View File

@ -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(

View File

@ -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
});

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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
}
}
}
`;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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);

View File

@ -1 +0,0 @@
export { default } from './Show.js';

View File

@ -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"