import React, { Component, 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 Component { 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 (
Share your non-freeCodeCamp projects, articles or accepted pull requests.