feat(portfolio): Add portfolio settings

This commit is contained in:
Bouncey
2018-09-20 16:27:06 +01:00
committed by Stuart Taylor
parent 9f4430eced
commit ef0428b396
4 changed files with 368 additions and 5 deletions

View File

@ -17,6 +17,7 @@ import About from '../components/settings/About';
import Privacy from '../components/settings/Privacy';
import Email from '../components/settings/Email';
import Internet from '../components/settings/Internet';
import Portfolio from '../components/settings/Portfolio';
const propTypes = {
about: PropTypes.string,
@ -28,6 +29,15 @@ const propTypes = {
name: PropTypes.string,
picture: PropTypes.string,
points: PropTypes.number,
portfolio: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
url: PropTypes.string,
image: PropTypes.string,
description: PropTypes.string
})
),
sendQuincyEmail: PropTypes.bool,
showLoading: PropTypes.bool,
submitNewAbout: PropTypes.func.isRequired,
@ -35,6 +45,7 @@ const propTypes = {
toggleNightMode: PropTypes.func.isRequired,
twitter: PropTypes.string,
updateInternetSettings: PropTypes.func.isRequired,
updatePortfolio: PropTypes.func.isRequired,
updateQuincyEmail: PropTypes.func.isRequired,
username: PropTypes.string,
website: PropTypes.string
@ -59,7 +70,8 @@ const mapStateToProps = createSelector(
githubProfile,
linkedin,
twitter,
website
website,
portfolio
}
) => ({
email,
@ -76,7 +88,8 @@ const mapStateToProps = createSelector(
githubProfile,
linkedin,
twitter,
website
website,
portfolio
})
);
@ -86,6 +99,7 @@ const mapDispatchToProps = dispatch =>
submitNewAbout,
toggleNightMode: theme => updateUserFlag({ theme }),
updateInternetSettings: updateUserFlag,
updatePortfolio: updateUserFlag,
updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail })
},
dispatch
@ -111,7 +125,9 @@ function ShowSettings(props) {
linkedin,
twitter,
website,
updateInternetSettings
updateInternetSettings,
portfolio,
updatePortfolio
} = props;
if (showLoading) {
@ -182,9 +198,9 @@ function ShowSettings(props) {
website={website}
/>
<Spacer />
{/* <PortfolioSettings />
<Portfolio portfolio={portfolio} updatePortfolio={updatePortfolio} />
<Spacer />
<Honesty />
{/* <Honesty />
<Spacer />
<CertificationSettings />
<Spacer />

View File

@ -0,0 +1,342 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import nanoid from 'nanoid';
import {
Button,
FormGroup,
ControlLabel,
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { findIndex, find, isEqual } from 'lodash';
import isURL from 'validator/lib/isURL';
import { hasProtocolRE } from '../../utils';
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
import SectionHeader from './SectionHeader';
import BlockSaveButton from '../helpers/form/BlockSaveButton';
import './portfolio.css';
const propTypes = {
picture: PropTypes.string,
portfolio: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
image: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
})
),
updatePortfolio: PropTypes.func.isRequired,
username: PropTypes.string
};
function createEmptyPortfolio() {
return {
id: nanoid(),
title: '',
description: '',
url: '',
image: ''
};
}
function createFindById(id) {
return p => p.id === id;
}
const mockEvent = {
preventDefault() {}
};
class PortfolioSettings extends PureComponent {
constructor(props) {
super(props);
const { portfolio = [] } = props;
this.state = {
portfolio: [...portfolio]
};
}
createOnChangeHandler = (id, key) => e => {
e.preventDefault();
const userInput = e.target.value.slice();
return this.setState(state => {
const { portfolio: currentPortfolio } = state;
const mutatblePortfolio = currentPortfolio.slice(0);
const index = findIndex(currentPortfolio, p => p.id === id);
mutatblePortfolio[index] = {
...mutatblePortfolio[index],
[key]: userInput
};
return { portfolio: mutatblePortfolio };
});
};
handleSubmit = e => {
e.preventDefault();
const { updatePortfolio } = this.props;
const { portfolio } = this.state;
return updatePortfolio({ portfolio });
};
handleAdd = () => {
return this.setState(state => ({
portfolio: [createEmptyPortfolio(), ...state.portfolio]
}));
};
handleRemoveItem = id => {
return this.setState(
state => ({
portfolio: state.portfolio.filter(p => p.id !== id)
}),
() => this.handleSubmit(mockEvent)
);
};
isFormPristine = id => {
const { portfolio } = this.state;
const { portfolio: originalPortfolio } = this.props;
const original = find(originalPortfolio, createFindById(id));
if (!original) {
return false;
}
const edited = find(portfolio, createFindById(id));
return isEqual(original, edited);
};
isFormValid = id => {
const { portfolio } = this.state;
const toValidate = find(portfolio, createFindById(id));
if (!toValidate) {
return false;
}
const { title, url, image, description } = toValidate;
const { state: titleState } = this.getTitleValidation(title);
const { state: urlState } = this.getUrlValidation(url);
const { state: imageState } = this.getUrlValidation(image, true);
const { state: descriptionState } = this.getDescriptionValidation(
description
);
return [titleState, imageState, urlState, descriptionState]
.filter(Boolean)
.every(state => state === 'success');
};
getDescriptionValidation(description) {
const len = description.length;
const charsLeft = 288 - len;
if (charsLeft < 0) {
return {
state: 'error',
message: 'There is a maxiumum limit of 288 characters, you have 0 left'
};
}
if (charsLeft < 41 && charsLeft > 0) {
return {
state: 'warning',
message:
'There is a maxiumum limit of 288 characters, you have ' +
charsLeft +
' left'
};
}
if (charsLeft === 288) {
return { state: null, message: '' };
}
return { state: 'success', message: '' };
}
getTitleValidation(title) {
if (!title) {
return { state: 'error', message: 'A title is required' };
}
const len = title.length;
if (len < 2) {
return { state: 'error', message: 'Title is too short' };
}
if (len > 144) {
return { state: 'error', message: 'Title is too long' };
}
return { state: 'success', message: '' };
}
getUrlValidation(maybeUrl, isImage) {
const len = maybeUrl.length;
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
return { state: 'error', message: 'URL must start with http or https' };
}
if (isImage && !maybeUrl) {
return { state: null, message: '' };
}
if (isImage && !(/\.(png|jpg|jpeg|gif)$/).test(maybeUrl)) {
return {
state: 'error',
message: 'URL must link directly to an image file'
};
}
return isURL(maybeUrl)
? { state: 'success', message: '' }
: { state: 'warning', message: 'Please use a valid URL' };
}
renderPortfolio = (portfolio, index, arr) => {
const { id, title, description, url, image } = portfolio;
const pristine = this.isFormPristine(id);
const {
state: titleState,
message: titleMessage
} = this.getTitleValidation(title);
const { state: urlState, message: urlMessage } = this.getUrlValidation(url);
const { state: imageState, message: imageMessage } = this.getUrlValidation(
image,
true
);
const {
state: descriptionState,
message: descriptionMessage
} = this.getDescriptionValidation(description);
return (
<div key={id}>
<FullWidthRow>
<form onSubmit={this.handleSubmit}>
<FormGroup
controlId={`${id}-title`}
validationState={
pristine || (!pristine && !title) ? null : titleState
}
>
<ControlLabel>Title</ControlLabel>
<FormControl
onChange={this.createOnChangeHandler(id, 'title')}
required={true}
type='text'
value={title}
/>
{titleMessage ? <HelpBlock>{titleMessage}</HelpBlock> : null}
</FormGroup>
<FormGroup
controlId={`${id}-url`}
validationState={
pristine || (!pristine && !url) ? null : urlState
}
>
<ControlLabel>URL</ControlLabel>
<FormControl
onChange={this.createOnChangeHandler(id, 'url')}
required={true}
type='url'
value={url}
/>
{urlMessage ? <HelpBlock>{urlMessage}</HelpBlock> : null}
</FormGroup>
<FormGroup
controlId={`${id}-image`}
validationState={pristine ? null : imageState}
>
<ControlLabel>Image</ControlLabel>
<FormControl
onChange={this.createOnChangeHandler(id, 'image')}
type='url'
value={image}
/>
{imageMessage ? <HelpBlock>{imageMessage}</HelpBlock> : null}
</FormGroup>
<FormGroup
controlId={`${id}-description`}
validationState={pristine ? null : descriptionState}
>
<ControlLabel>Description</ControlLabel>
<FormControl
componentClass='textarea'
onChange={this.createOnChangeHandler(id, 'description')}
value={description}
/>
{descriptionMessage ? (
<HelpBlock>{descriptionMessage}</HelpBlock>
) : null}
</FormGroup>
<BlockSaveButton
disabled={
pristine ||
!title ||
!isURL(url, {
protocols: ['http', 'https'],
/* eslint-disable camelcase */
require_tld: true,
require_protocol: true
/* eslint-enable camelcase */
})
}
>
Save this portfolio item
</BlockSaveButton>
<ButtonSpacer />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-delete-portfolio'
onClick={() => this.handleRemoveItem(id)}
type='button'
>
Remove this portfolio item
</Button>
</form>
{index + 1 !== arr.length && (
<Fragment>
<Spacer />
<hr />
<Spacer />
</Fragment>
)}
</FullWidthRow>
</div>
);
};
render() {
const { portfolio = [] } = this.state;
return (
<section id='portfolio-settings'>
<SectionHeader>Portfolio Settings</SectionHeader>
<FullWidthRow>
<div className='portfolio-settings-intro'>
<p className='p-intro'>
Share your non-FreeCodeCamp projects, articles or accepted pull
requests.
</p>
</div>
</FullWidthRow>
<FullWidthRow>
<ButtonSpacer />
<Button
block={true}
bsSize='lg'
bsStyle='primary'
onClick={this.handleAdd}
type='button'
>
Add a new portfolio Item
</Button>
</FullWidthRow>
<Spacer size={2} />
{portfolio.length ? portfolio.map(this.renderPortfolio) : null}
</section>
);
}
}
PortfolioSettings.displayName = 'PortfolioSettings';
PortfolioSettings.propTypes = propTypes;
export default PortfolioSettings;

View File

@ -0,0 +1,4 @@
.btn-delete-portfolio {
background-color: #fff;
color: #f11e00
}

View File

@ -3,3 +3,4 @@
// before we try to validate
export const maybeEmailRE = /.*@.*\.\w\w/;
export const maybeUrlRE = /https?:\/\/.*\..*/;
export const hasProtocolRE = /^http/;