chore(learn): Merge learn in to the client app

This commit is contained in:
Bouncey
2018-09-30 11:37:19 +01:00
committed by Stuart Taylor
parent 9e869a46fc
commit 5b254f3ad6
320 changed files with 9820 additions and 27605 deletions

View File

@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@freecodecamp/react-bootstrap';
function BlockSaveButton(props) {
return (
<Button block={true} 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,84 @@
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)}
style={{ width: '100%' }}
>
<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,86 @@
import React from 'react';
import { kebabCase, startCase } from 'lodash';
import PropTypes from 'prop-types';
import {
Alert,
Col,
ControlLabel,
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import './form-fields.css';
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 (
<div 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>
</div>
);
})}
</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-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,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DynamicForm /> snapshot 1`] = `
<form
id="dynamic-my-test-form"
style={
Object {
"width": "100%",
}
}
>
<div>
<div
className="inline-form-field"
>
<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-primary btn-block"
disabled={false}
type="submit"
>
Submit
</button>
</div>
</form>
`;

View File

@@ -0,0 +1,6 @@
.inline-form-field {
height: 100%;
display: flex;
align-items: center;
margin-bottom: 5px;
}

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