Created
December 25, 2018 16:11
-
-
Save lyquangthai1993/2b85d916a89b1f56e49ac06297708d8c to your computer and use it in GitHub Desktop.
Demo friends in service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* ServiceEditPage | |
* | |
*/ | |
//Lib | |
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { connect } from "react-redux"; | |
import { createStructuredSelector } from "reselect"; | |
import { compose } from "redux"; | |
import { Formik, Form, Field, FieldArray, getIn, ErrorMessage } from 'formik'; | |
import * as Yup from 'yup'; | |
import { WithContext as ReactTags } from 'react-tag-input'; | |
import _ from "lodash"; | |
//Saga, action, reducer, selector | |
import injectSaga from "utils/injectSaga"; | |
import injectReducer from "utils/injectReducer"; | |
import makeSelectServiceEditPage from "./selectors"; | |
import reducer from "./reducer"; | |
import saga from "./saga"; | |
import { | |
getServiceDetails, updateService, | |
addCoupon, updateCoupon, | |
addCert, updateCert, | |
addArea, updateArea, | |
addTag, deleteTag, | |
changeImage, | |
showModal | |
} from './actions'; | |
import { loadRepos } from '../App/actions'; | |
//css | |
import './style.scss'; | |
//Components | |
import SubmitButton from 'components/SubmitButton'; | |
import InputForm from "components/InputForm"; | |
import Dropdown from 'components/Dropdown'; | |
import GhostButton from 'components/GhostButton'; | |
import PurpleRoundButton from 'components/PurpleRoundButton'; | |
import moment from 'moment'; | |
import { Modal, ModalBody, UncontrolledTooltip } from 'reactstrap'; | |
import TagInput from "components/TagInput"; | |
import CouponItem from "components/CouponItem"; | |
import CropImage from 'components/CropImage'; | |
import Datepicker from 'components/Datepicker'; | |
const validateForm = Yup.object().shape({ | |
serviceName: Yup.string() | |
.required('Please enter service name'), | |
hourlyRate: Yup.string() | |
.required('Please enter hourly rate'), | |
minimumHours: Yup.string() | |
.required('Please enter minimum hours'), | |
commission: Yup.string() | |
.required('Please enter commission'), | |
couponList: Yup.array() | |
.of( | |
Yup.object().shape({ | |
couponName: Yup.string() | |
.required('Required'), // these constraints take precedence | |
couponCode: Yup.string() | |
.required('Required'), // these constraints take precedence | |
disCount: Yup.number() | |
.required('Required'), // these constraints take precedence | |
expiredAt: Yup.string() | |
.required('Required'), // these constraints take precedence | |
}) | |
) | |
.min(1, 'Minimum of 1 coupon'), | |
// 'couponCode': Yup.string() | |
// .required('') | |
// .min(8, 'Coupon Code is at least 8 characters'), | |
// 'certificationName': Yup.string() | |
// .required('') | |
// .min(8, 'Certification Name is at least 8 characters') | |
// .max(30, 'Certification Name is maximum at 30 characters') | |
// .matches(/(^[a-zA-Z0-9]{4,10}$)/, 'Invalid certification name (No special characters)'), | |
// 'cityAndCountry': Yup.string() | |
// .required('') | |
}); | |
/* eslint-disable react/prefer-stateless-function */ | |
export class ServiceEditPage extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
cropSrc: '', | |
showBackgroundModal: false | |
} | |
} | |
_disableButton(value, errors) { | |
//console.log(value); | |
console.log(errors); | |
//Loop through validation fields | |
const keys = [ | |
'serviceName', | |
'hourlyRate', | |
'minimumHours', | |
'commission', | |
'couponList' | |
]; | |
// for (let key of keys) { | |
// if (value[key] == null || errors[key] || !value[key].toString()) { | |
// //If this field has error or | |
// //console.log(key); | |
// return true; | |
// } | |
// } | |
return false; | |
} | |
componentWillMount() { | |
let url = window.location.href; | |
let temp = url.split('?id='); | |
let serviceID = temp[1]; | |
this.props.getService(serviceID); | |
this.props.startLoad(); | |
} | |
openFileBrowser = (type) => { | |
if (type == 'icon') { | |
this.refs.iconUploader.click(); | |
} | |
else { | |
this.refs.backgroundUploader.click(); | |
} | |
} | |
uploadImage = (e, type) => { | |
let file = e.target && e.target.files ? e.target.files.item(0) : null; | |
if (file) { | |
let reader = new FileReader(); | |
reader.onloadend = (e) => { | |
if (type == 'icon') { | |
this.props.changeImage('icon', e.target.result); | |
this.setState({ | |
iconFile: file, | |
}); | |
} | |
else { | |
this.setState({ | |
cropSrc: e.target.result | |
}, () => { | |
this.setState({ showBackgroundModal: true }); | |
}) | |
} | |
} | |
reader.readAsDataURL(file); | |
} | |
} | |
changeImage = (src, file) => { | |
this.props.changeImage('background', src); | |
this.setState({ | |
cropSrc: '', | |
backgroundFile: file, | |
showBackgroundModal: false | |
}); | |
} | |
closeModal = () => { | |
this.setState({ | |
cropSrc: '', | |
showBackgroundModal: false, | |
}); | |
} | |
handleTagsChange = (tagsList) => { | |
this.setState({ keywords: tagsList }); | |
} | |
submitForm = (e) => { | |
const { couponCodes, certifications, areas, keywords, deletedKeywords } = this.props.serviceeditpage; | |
let formData = new FormData(); | |
if (this.state.iconFile) { | |
formData.append('icon', this.state.iconFile); | |
} | |
if (this.state.backgroundFile) { | |
formData.append('background', this.state.backgroundFile); | |
} | |
formData.append('name', e.serviceName); | |
let rateHours = { | |
unit: "$", | |
value: e.hourlyRate | |
}; | |
formData.append('rateHours', JSON.stringify(rateHours)); | |
let minCharge = { | |
unit: "$", | |
value: e.hourlyRate * e.minimumHours | |
}; | |
formData.append('minCharge', JSON.stringify(minCharge)); | |
let minHours = { | |
unit: "", | |
value: e.minimumHours, | |
}; | |
formData.append('minHours', JSON.stringify(minHours)); | |
let commission = { | |
unit: "%", | |
value: e.commission | |
}; | |
formData.append('commission', JSON.stringify(commission)); | |
formData.append('couponCodes', JSON.stringify(couponCodes)); | |
let array1 = keywords; | |
let newKeywords = keywords.concat(deletedKeywords); | |
let uploadKeywords = newKeywords.map((keyword, index) => { | |
return { | |
id: keyword.id, | |
keyName: keyword.text, | |
action: keyword.action | |
} | |
}) | |
formData.append('keywords', JSON.stringify(uploadKeywords)); | |
formData.append('certifications', JSON.stringify(certifications)); | |
let newAreas = areas.map((area, index) => { | |
let suburbs = area.suburbs.split(','); | |
return { | |
...area, | |
suburbs: suburbs, | |
} | |
}); | |
formData.append('areas', JSON.stringify(newAreas)); | |
this.setState({ updateData: formData }, () => { | |
this.props.showModal('confirm', true); | |
}) | |
} | |
onChangeCouponCode = (coupon, type, value) => { | |
let newCoupon = coupon; | |
newCoupon[type] = value; | |
if (coupon.id === '') { | |
newCoupon['action'] = 'add'; | |
} | |
else { | |
newCoupon['action'] = 'edit'; | |
} | |
this.props.updateCoupon(coupon.id, newCoupon); | |
} | |
render() { | |
const { | |
serviceDetails, couponCodes, errors, keywords, certifications, areas, | |
backgroundSrc, iconSrc, showConfirmModal | |
} = this.props.serviceeditpage; | |
let apiErrors = errors; | |
return ( | |
<div className="edit-service-profile"> | |
<div className="header-edit-add-page edit-service-header"> | |
<div className="action"> | |
<div className="return" id='return'> | |
<span className="icon-arrow-left" onClick={e => { this.props.history.goBack(); }}></span> | |
</div> | |
<UncontrolledTooltip className="fixle-tooltip" placement="bottom" target="return"> | |
Back</UncontrolledTooltip> | |
</div> | |
<div className="title"> | |
<span>Edit Service</span> | |
</div> | |
</div> | |
<input type="file" id="file" ref="iconUploader" style={{ display: "none" }} onChange={evt => { | |
this.uploadImage(evt, 'icon'); | |
evt.target.value = null; | |
}} /> | |
<input type="file" id="file" ref="backgroundUploader" style={{ display: "none" }} onChange={evt => { | |
this.uploadImage(evt, 'background'); | |
evt.target.value = null; | |
}} /> | |
<Formik ref={ref => { this.formik = ref }} | |
initialValues={{ | |
serviceName: serviceDetails ? serviceDetails.name : '', | |
hourlyRate: serviceDetails ? serviceDetails.rateHours.value : '', | |
minimumHours: serviceDetails ? serviceDetails.minHours.value : '', | |
minimumCharge: serviceDetails ? serviceDetails.minCharge.value : '', | |
commission: serviceDetails ? serviceDetails.commission.value : '', | |
couponList: serviceDetails ? couponCodes : [], | |
}} | |
enableReinitialize={true} | |
validationSchema={validateForm} | |
onSubmit={e => { | |
this.submitForm(e); | |
}}> | |
{({ | |
values, | |
dirty, | |
errors, | |
touched, | |
handleChange, | |
handleBlur, | |
handleSubmit, | |
}) => ( | |
<Form onSubmit={handleSubmit}> | |
<div className="content-add-edit edit-service-content"> | |
<div className="information service-info"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Service Information</span> | |
</div> | |
</div> | |
<div className="col-md-5 service-details"> | |
<div className="details-wrapper"> | |
<div className="service-icon"> | |
<div className="icon-wrapper"> | |
<img className="icon-uploaded" src={iconSrc} alt="icon" /> | |
</div> | |
<span className="change-avatar add-icon" onClick={e => { this.openFileBrowser('icon') }}>Add Service Icon</span> | |
</div> | |
<div className="details"> | |
{/* SERVICE NAME */} | |
<InputForm | |
label="service name" | |
name='serviceName' | |
value={values.serviceName} | |
error={errors.serviceName} | |
touched={touched.serviceName} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onBlur={handleBlur} | |
placeholder={'Service name'} /> | |
{/* HOURLY RATE */} | |
<InputForm | |
label="hourly rate" | |
name={'hourlyRate'} | |
value={values.hourlyRate} | |
error={errors.hourlyRate} | |
touched={touched.hourlyRate} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onBlur={handleBlur} | |
placeholder={'Hourly rate'} | |
type={'number'} /> | |
{/* MINIMUM HOURS */} | |
<InputForm | |
label="minimum hours" | |
name={'minimumHours'} | |
value={values.minimumHours} | |
error={errors.minimumHours} | |
touched={touched.minimumHours} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onBlur={handleBlur} | |
placeholder={'Minimum hours'} | |
type={'number'} /> | |
{/* MINIMUM CHARGE */} | |
<InputForm | |
label="minimum charge" | |
name={'minimumCharge'} | |
value={(values.minimumHours === '' || values.hourlyRate === '') ? '' : values.minimumHours * values.hourlyRate} | |
error={errors.minimumCharge} | |
touched={touched.minimumCharge} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onBlur={handleBlur} | |
placeholder={'Minimum hours x hourly rate'} /> | |
{(apiErrors && apiErrors.length > 0) ? apiErrors.map((error, index) => { | |
return ( | |
<div key={error.errorCode} className="errors"> | |
<span className="icon-error"></span> | |
<div className="error-item"> | |
<span>{error.errorMessage}</span> | |
</div> | |
</div> | |
); | |
}) : []} | |
</div> | |
</div> | |
</div> | |
<div className="col-md-3 service-background"> | |
<div className="background-wrapper"> | |
<div className="title"> | |
<span>Background Image</span> | |
</div> | |
<div className="background-container"> | |
<img className="background-uploaded" src={backgroundSrc} alt="icon" /> | |
</div> | |
<div className="upload-background"> | |
<span onClick={e => { this.openFileBrowser('background'); }}>Change Bg Image</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="information fixle-charge"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Fixle Charge</span> | |
</div> | |
</div> | |
<div className="col-md-8 right"> | |
<div className="details"> | |
{/* COMMISSION */} | |
<InputForm | |
label="commission per booking" | |
name={'commission'} | |
type='number' | |
value={values.commission} | |
error={errors.commission} | |
touched={touched.commission} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onBlur={handleBlur} | |
placeholder={'Commission per booking'} | |
type={'number'} /> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="information coupon-code"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Coupon Code</span> | |
</div> | |
</div> | |
<div className="col-md-8 right"> | |
{values.couponList.map((coupon, index) => { | |
return ( | |
// <Field | |
// key={index} | |
// name={`couponList.${index}`} | |
// component={CouponItem} | |
// onChange={evt => { | |
// handleChange(evt); | |
// }} | |
// { | |
// ...{ | |
// index: index, | |
// nameInput: `couponList[${index}]`, | |
// couponName: coupon.name, | |
// couponCode: coupon.code, | |
// disCountType: coupon.disCountType, | |
// disCount: coupon.disCount, | |
// expiredAt: coupon.expiredAt, | |
// hidden: coupon.action === 'delete', | |
// onHandleChange: (id, type, value) => { | |
// let newCoupon = coupon; | |
// newCoupon[type] = value; | |
// if (coupon.id === '') { | |
// newCoupon['action'] = 'add'; | |
// } | |
// else { | |
// newCoupon['action'] = 'edit'; | |
// } | |
// this.props.updateCoupon(id, newCoupon); | |
// }, | |
// delete: (id) => { | |
// let newCoupon = coupon; | |
// coupon['action'] = 'delete'; | |
// this.props.updateCoupon(id, newCoupon); | |
// } | |
// }} | |
// /> | |
<CouponItem | |
name={`couponList[${index}]`} | |
key={index} | |
index={index} | |
couponName={coupon.name} | |
couponCode={coupon.code} | |
disCount={coupon.disCount} | |
disCountType={coupon.disCountType} | |
expiryDate={coupon.expiredAt} | |
onChange={evt => { | |
handleChange(evt); | |
}} | |
onHandleChange={(id, type, value) => { | |
let newCoupon = coupon; | |
newCoupon[type] = value; | |
if (coupon.id === '') { | |
newCoupon['action'] = 'add'; | |
} | |
else { | |
newCoupon['action'] = 'edit'; | |
} | |
this.props.updateCoupon(id, newCoupon); | |
}} | |
delete={id => { | |
let newCoupon = coupon; | |
coupon['action'] = 'delete'; | |
this.props.updateCoupon(id, newCoupon); | |
}} | |
hidden={coupon.action === 'delete'} | |
value={values.commission} | |
/> | |
) | |
})} | |
<div className="row addition-action"> | |
<PurpleRoundButton | |
title={'Add Coupon'} | |
className="btn-purple-round add-coupon" | |
type="button" | |
onClick={e => { this.props.addCoupon(); }} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* KEYWORDS */} | |
<div className="information keywords"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Keywords</span> | |
</div> | |
</div> | |
<div className="col-md-8 right"> | |
<div className="details"> | |
<div className="info-item"> | |
<div className="title"> | |
<span>Keywords</span> | |
</div> | |
<div className="data list-tags"> | |
<ReactTags | |
autofocus={false} | |
tags={keywords} | |
handleDelete={index => { | |
this.props.deleteTag(index); | |
}} | |
handleAddition={tag => { | |
tag.id = ''; | |
tag['action'] = 'add'; | |
this.props.addTag(tag); | |
}} | |
placeholder={'Keywords'} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="information certification"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Certification Required</span> | |
</div> | |
</div> | |
<div className="col-md-8 right"> | |
{certifications.map((cert, index) => ( | |
<div key={cert.id} className="certification-item row" hidden={cert.action === 'delete'}> | |
<div className="col-md-10"> | |
<InputForm | |
label="certification name" | |
value={cert.content} | |
onChange={evt => { | |
let newItem = cert; | |
newItem.content = evt.target.value; | |
if (cert.id === '') { | |
newItem.action = 'add'; | |
} | |
else { | |
newItem.action = 'edit'; | |
} | |
this.props.updateCert(index, newItem); | |
}} | |
placeholder={'Certification name'} /> | |
</div> | |
<div className="col-md-2"> | |
<div className="delete-tab"> | |
<span className="icon-bin" onClick={e => { | |
let newItem = cert; | |
newItem.action = 'delete'; | |
this.props.updateCert(index, newItem); | |
}}></span> | |
</div> | |
</div> | |
</div> | |
))} | |
<div className="row addition-action"> | |
<PurpleRoundButton title={'Add Certification'} className="btn-purple-round add-certification" onClick={e => { this.props.addCert(); }} type="button" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="information areas-supplied"> | |
<div className="row"> | |
<div className="col-md-4 left"> | |
<div className="title"> | |
<span>Areas supplied</span> | |
</div> | |
</div> | |
<div className="col-md-8 right"> | |
{areas.map((area, index) => { | |
return ( | |
<div key={index} className="area-item row"> | |
<div className="col-md-10"> | |
<InputForm | |
label="city and country" | |
value={area.country} | |
onChange={evt => { | |
let newItem = area; | |
newItem.city = evt.target.value; | |
newItem.country = evt.target.value; | |
if (area.id === '') { | |
newItem.action = 'add'; | |
} | |
else { | |
newItem.action = 'edit'; | |
} | |
this.props.updateArea(index, newItem); | |
}} | |
placeholder={'City and country'} /> | |
<InputForm | |
label="suburbs" | |
value={area.suburbs} | |
onChange={evt => { | |
let newItem = area; | |
newItem.suburbs = evt.target.value; | |
if (area.id === '') { | |
newItem.action = 'add'; | |
} | |
else { | |
newItem.action = 'edit'; | |
} | |
this.props.updateArea(index, newItem); | |
}} | |
placeholder={'Suburbs'} /> | |
</div> | |
<div className="col-md-2"> | |
<div className="delete-tab"> | |
<span className="icon-bin" onClick={e => { | |
let newItem = area; | |
newItem.action = 'delete'; | |
this.props.updateArea(index, newItem); | |
}}></span> | |
</div> | |
</div> | |
</div> | |
) | |
})} | |
<div className="row addition-action"> | |
<PurpleRoundButton title={'Add Area'} className="btn-purple-round add-area" onClick={e => { this.props.addArea(); }} type="button" /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="footer edit-service-footer"> | |
<GhostButton className="btn-ghost cancel" title={'Cancel'} onClick={e => { this.props.history.goBack(); }} type="button" /> | |
<SubmitButton type={'submit'} disabled={this._disableButton(values, errors)} content={'Save'} /> | |
</div> | |
</Form> | |
)} | |
</Formik> | |
{(this.state.cropSrc && this.state.cropSrc != '') && | |
<CropImage | |
show={this.state.showBackgroundModal} | |
cropSrc={this.state.cropSrc} | |
closeModal={this.closeModal} | |
changeImage={this.changeImage} | |
/> | |
} | |
<Modal isOpen={showConfirmModal} className="logout-modal"> | |
<ModalBody> | |
<div className="add-success"> | |
<div className="upper"> | |
<div className="title"> | |
<span>Save This Change</span> | |
</div> | |
<div className="description"> | |
<span>Are you sure want to save this change. This action could influence on all data involved.</span> | |
</div> | |
</div> | |
<div className="lower"> | |
<GhostButton className="btn-ghost cancel" title={'Cancel'} onClick={e => { this.props.showModal('confirm', false); }} /> | |
<PurpleRoundButton className="btn-purple-round save" title={'Save'} onClick={e => { | |
if (this.state.updateData) { | |
this.props.showModal('confirm', false); | |
this.props.update(serviceDetails._id, this.state.updateData); | |
} | |
}} /> | |
</div> | |
</div> | |
</ModalBody> | |
</Modal> | |
</div> | |
); | |
} | |
} | |
ServiceEditPage.propTypes = { | |
dispatch: PropTypes.func, | |
getService: PropTypes.func, | |
update: PropTypes.func, | |
addCoupon: PropTypes.func, | |
updateCoupon: PropTypes.func, | |
addCert: PropTypes.func, | |
updateCert: PropTypes.func, | |
addArea: PropTypes.func, | |
updateArea: PropTypes.func, | |
changeImage: PropTypes.func, | |
startLoad: PropTypes.func, | |
addTag: PropTypes.func, | |
deleteTag: PropTypes.func, | |
showModal: PropTypes.func | |
}; | |
const mapStateToProps = createStructuredSelector({ | |
serviceeditpage: makeSelectServiceEditPage() | |
}); | |
function mapDispatchToProps(dispatch) { | |
return { | |
getService: (id) => { | |
dispatch(getServiceDetails(id)); | |
}, | |
update: (id, updateData) => { | |
dispatch(updateService(id, updateData)); | |
}, | |
addCoupon: () => { | |
dispatch(addCoupon()); | |
}, | |
updateCoupon: (id, newItem) => { | |
dispatch(updateCoupon(id, newItem)); | |
}, | |
addCert: () => { | |
dispatch(addCert()); | |
}, | |
updateCert: (id, newItem) => { | |
dispatch(updateCert(id, newItem)); | |
}, | |
addArea: () => { | |
dispatch(addArea()); | |
}, | |
updateArea: (id, newItem) => { | |
dispatch(updateArea(id, newItem)); | |
}, | |
changeImage: (objectType, src) => { | |
dispatch(changeImage(objectType, src)); | |
}, | |
startLoad: () => { | |
dispatch(loadRepos()); | |
}, | |
addTag: (newTag) => { | |
dispatch(addTag(newTag)); | |
}, | |
deleteTag: (id) => { | |
dispatch(deleteTag(id)); | |
}, | |
showModal: (modal, value) => { | |
dispatch(showModal(modal, value)); | |
} | |
}; | |
} | |
const withConnect = connect( | |
mapStateToProps, | |
mapDispatchToProps | |
); | |
const withReducer = injectReducer({ key: "serviceEditPage", reducer }); | |
const withSaga = injectSaga({ key: "serviceEditPage", saga }); | |
export default compose( | |
withReducer, | |
withSaga, | |
withConnect | |
)(ServiceEditPage); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment