Skip to content

Instantly share code, notes, and snippets.

@SvitlanaShepitsena
Created January 9, 2020 12:51
Show Gist options
  • Save SvitlanaShepitsena/0a755feaddc71e77ed90789363082308 to your computer and use it in GitHub Desktop.
Save SvitlanaShepitsena/0a755feaddc71e77ed90789363082308 to your computer and use it in GitHub Desktop.
PostForm created with RenderProps Approach
import React, { Component, createRef } from 'react'
import styled from 'styled-components'
import CKeditor from 'react-ckeditor-component'
import { withRouter } from 'next/router'
import { SortableContainer, arrayMove } from 'react-sortable-hoc'
import { Mutation, Query } from 'react-apollo'
import axios from 'axios'
import CancelSubmitButtons from '../../common/buttons/CancelSubmitButtons'
import RemoveButton from '../../common/buttons/RemoveButton'
// todo: remove on prod
import { Field, Formik } from 'formik'
import Dropzone from 'react-dropzone'
import {
Grid,
Card,
Form,
Header,
Icon,
Input,
Loader,
Message
} from 'semantic-ui-react'
import parseVideo from '../../filters/parseVideo'
import combineVideo from '../../filters/combineVideo'
import InputField from './InputField'
import VideoContainer from '../../common/media/video/VideoContainer'
import StyledPostForm from '../../common/Styled_Components/Styled_PostForm'
import { CREATE_POST } from '../../../api/mutations/post/createPost'
import TYPE_SECTIONS from '../../../api/queries/post/typeSections'
import PROJECT_ADDRESSES from '../../../api/queries/project/projectAddress'
import PROJECT_TYPES from '../../../api/queries/post/projectTypes'
import * as yup from 'yup'
import withUser from '../../../lib/withUser'
import DatePicker from 'react-datepicker'
import SortableImage from './SortableImage'
import uploadImage from '../../../lib/utils/uploadImage'
import ImageBackground from '../../common/media/images/ImageBackground'
import moment from 'moment'
import constants from '../../../api/constants'
const SortableList = SortableContainer(
({ items, deleteGalImage, changeCaption, loadingImage }) => {
return (
<Grid stackable columns={4}>
{items.map((value, index) => (
<SortableImage
key={`item-${index}`}
index={index}
loadingImage={loadingImage}
imageIndex={index}
image={value}
delete={deleteGalImage}
changeCaption={changeCaption}
/>
))}
</Grid>
)
}
)
let newPostSchema = yup.object().shape({
customAuthor: yup.string(),
name: yup
.string()
.required('Title is required')
.max(160, 'Title should be less than 160 symbols'),
email: yup.string().email('Please provide a valid email'),
phone: yup.string(),
description: yup
.string()
.required('Description is required')
.max(100, 'Description should be less than 150 symbols'),
tags: yup.string()
})
class PostForm extends Component {
state = {
loadingImage: -1,
content: 'Post Content',
id: '',
projectType: '',
projectAddress: '',
fullVideo: '',
videoSource: '',
startTime: moment()
.startOf('hour')
.add(2, 'hours')
.format('MM/DD/YYYY hh:mm A'),
endTime: moment()
.startOf('hour')
.add(4, 'hours')
.format('MM/DD/YYYY hh:mm A'),
video: '',
files: [],
imageFile: '',
imagePrev: '',
image: '',
contentError: false,
loading: false,
startTimeError: false,
endTimeError: false
}
deleteGalImage = (index) => {
const files = [...this.state.files]
files.splice(index, 1)
this.setState({ files })
}
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ files }) => ({
files: arrayMove(files, oldIndex, newIndex)
}))
}
componentDidMount () {
if (this.props.post) {
let startTime = new Date(this.props.post.startTime)
let endTime = new Date(this.props.post.endTime)
if (this.props.post && startTime) {
this.setState({
...this.props.post,
startTime: `${startTime.toLocaleDateString()} ${startTime.toLocaleTimeString()}`,
endTime: `${endTime.toLocaleDateString()} ${endTime.toLocaleTimeString()}`,
files: this.props.post.galImages
})
}
}
}
handleChangeDatePickerStart = (date) => {
this.setState({
startTime: date.toLocaleDateString() + ' ' + date.toLocaleTimeString(),
startTimeError: false
})
}
handleChangeDatePickerEnd = (date) => {
this.setState({
endTime: date.toLocaleDateString() + ' ' + date.toLocaleTimeString(),
endTimeError: false
})
}
handleChange = (event, data) => {
if (!event.target.name) {
this.setState({ section: data.value, contentError: false })
} else {
this.setState({ [event.target.name]: event.target.value })
let { name, value } = event.target
if (name === 'fullVideo') {
var videoObject = parseVideo(value)
this.setState({
videoSource: videoObject.type,
video: videoObject.key
})
}
}
}
handleDrop = (files) => {
this.setState({
image: files[0].preview,
imagePrev: files[0].preview,
imageFile: files[0]
})
}
handleDropGallery = (files) => {
let filesWithCaption = files.map((file) => ({
file: file,
caption: '',
order: -1
}))
this.setState((prevState) => ({
files: [...prevState.files, ...filesWithCaption]
}))
}
changeCaption = (index, caption) => {
let galImages = [...this.state.files]
galImages[index].caption = caption
this.setState({ files: galImages })
}
onBlur = (evt) => {
if (this.state.content.length === 0) {
this.setState({ contentError: true })
}
}
onChange = (evt) => {
let newContent = evt.editor.getData()
this.setState({
content: newContent
})
}
render () {
let {
id,
image,
projectAddress,
projectType,
content,
video,
imagePrev,
fullVideo,
videoSource,
startTime,
endTime
} = this.state
const { postType, post } = this.props
let authorNameLabel =
this.props.postType === 'Classified' || this.props.postType === 'Event'
? 'Contact Name'
: 'Custom Author Name'
let authorNamePlaceholder =
this.props.postType === 'Classified' || this.props.postType === 'Event'
? 'Please Enter a Contact Name'
: 'Custom Author Name (Optional)'
let postTitleLabel = `${this.props.postType} Title`
let postTitlePlaceholder = `Give Your ${this.props.postType} a Title`
let postDescriptionLabel = `${this.props.postType} Description`
let postDescriptionPlaceholder = `Give Your ${
this.props.postType
} a Short Description`
let postSectionLabel = `${this.props.postType} Section`
const descriptionMessage = `Short description will appear on an ${
this.props.postType
} thumb and on a web search description of your ${this.props.postType}`
const videoMessage = `A video will always appear on an ${
this.props.postType
} thumbnail even if you have another media (gallery/images).`
const coverImageMessage = `Cover image will appear on an article thumb ONLY if the article does not have photo gallery or video"`
return (
<Mutation mutation={CREATE_POST}>
{(createPost, { loading, error, data }) => (
<Query
query={TYPE_SECTIONS}
variables={{
postType: post ? post.postType : postType
}}
fetchPolicy='cache-and-network'
notifyOnNetworkStatusChange
>
{({ data, loading, error }) => {
if (loading || error) {
return null
}
let { typeSections } = data
let semanticTypes
if (typeSections) {
semanticTypes = typeSections.map((typeSection) => {
return {
key: typeSection.id,
text: typeSection.name,
value: typeSection.id
}
})
}
return (
<Query
query={PROJECT_TYPES}
fetchPolicy='cache-and-network'
notifyOnNetworkStatusChange
>
{({ data, loading, error }) => {
if (loading || error) {
return null
}
let { projectTypes } = data
let projTypeSelect
if (projectTypes) {
projTypeSelect = projectTypes.map((projType) => {
return {
key: projType.id,
text: projType.name,
value: projType.id
}
})
}
return (
<Query
query={PROJECT_ADDRESSES}
fetchPolicy='cache-and-network'
notifyOnNetworkStatusChange
>
{({ data, loading, error }) => {
if (loading || error || !data) {
return null
}
let { projectAddresses } = data
let projectAddressesSelect
if (projectAddresses && projectAddresses.length) {
projectAddressesSelect = projectAddresses.map(
(pa) => {
return {
key: pa.id,
text:
(pa.house && `${pa.house} `) +
(pa.street && `${pa.street} `) +
(pa.city && `${pa.city} `) +
(pa.zip && `${pa.zip} `),
value: pa.id
}
}
)
}
return (
<>
{loading && <Loader />}
{error && (
<span>
Error....
{error.toString()}
</span>
)}
<Formik
initialValues={{
postType: this.props.postType,
section:
post && post.section
? post.section.id
: (Boolean(
typeSections && typeSections.length
) &&
typeSections[0].id) ||
'',
projectType: post
? post.projectType.id
: (Boolean(
projectTypes && projectTypes.length
) &&
projectTypes[0].key) ||
'',
projectAddress: post
? post.projectAddress.id
: (Boolean(
projectAddresses &&
projectAddresses.length
) &&
projectAddresses[0].key) ||
'',
name: post
? post.name
: `Article Title ${Math.random()}`,
email: post
? post.email
: this.props.user.email,
phone: post ? post.phone : '',
address: post ? post.address : '',
tickets: post
? post.tickets
: `www.tickets.com`,
customAuthor: post
? post.customAuthor
: constants.COMPANY_NAME,
description: post
? post.description
: 'Description',
content: post ? post.content : '',
tags: post
? post.tags.map((t) => t.name).join(' ')
: ''
}}
onSubmit={async (
{
postType,
projectAddress,
projectType,
section,
address,
tickets,
name,
email,
phone,
customAuthor,
description,
tags
},
{
setSubmitting,
setFieldValue,
setFieldError,
setStatus
}
) => {
setSubmitting(true)
if (postType === 'Events') {
postType = 'Event'
}
if (section && section.id) {
let refCopy = section.id
section = section.id
setFieldValue({ section: refCopy })
}
const formData = new FormData()
formData.append('file', this.state.imageFile)
formData.append(
'upload_preset',
process.env.CLOUDINARY_PRESET
) // Replace the preset name with your own
formData.append(
'api_key',
process.env.CLOUDINARY_KEY
) // Replace API key with your own Cloudinary key
formData.append(
'timestamp',
(Date.now() / 1000) | 0
)
let response
if (this.state.imageFile) {
try {
response = await axios.post(
`https://api.cloudinary.com/v1_1/${
process.env.CLOUDINARY_NAME
}/image/upload`,
formData,
{
headers: {
'X-Requested-With':
'XMLHttpRequest'
}
}
)
} catch (error) {
console.log(error)
}
}
if (this.state.files.length > 0) {
for (
let i = 0;
i < this.state.files.length;
i++
) {
this.setState({ loadingImage: i })
const file = this.state.files[i]
let fileUrl = await uploadImage(file)
const stateFiles = [...this.state.files]
stateFiles[i] = {
url: fileUrl,
caption: file.caption,
order: i
}
this.setState({ files: stateFiles })
}
}
if (
postType === 'Event' &&
(this.state.startTime === '' ||
this.state.endTime === '')
) {
if (this.state.startTime === '') {
this.setState({ startTimeError: true })
}
if (this.state.endTime === '') {
this.setState({
endTimeError: true
})
}
setSubmitting(false)
} else {
if (!projectType) {
projectType = projectTypes[0].id
}
if (!projectAddress) {
projectAddress = projectAddresses[0].id
}
createPost({
variables: {
id,
postType: post
? post.postType
: postType,
projectType,
projectAddress,
email,
address,
startTime,
endTime,
tickets,
phone,
name,
customAuthor,
section,
description,
content,
image:
response &&
response.data &&
response.data.secure_url
? response.data.secure_url
: image,
video,
videoSource,
tags,
galImages: JSON.stringify(
this.state.files
)
}
})
.then((res) => {
let url =
this.props.postType
.toLowerCase()
.indexOf('blog') > -1
? this.props.postType.toLowerCase()
: this.props.postType.toLowerCase() +
's'
// console.log('congratulations!!!')
this.props.router.push(`/${url}`)
})
.catch((err) => {
setStatus({
msg: err.message
})
setSubmitting(false)
})
.finally(() => setSubmitting(false))
}
}}
validationSchema={newPostSchema}
render={({
values,
status,
handleSubmit,
isSubmitting,
setFieldValue,
handleChange,
handleBlur,
errors,
touched
}) => {
if (image) {
imagePrev = image
}
if (videoSource && video) {
fullVideo = combineVideo(video, videoSource)
}
return (
<StyledPostForm onSubmit={handleSubmit}>
<FixedNav>
{this.state.loading || loading ? (
<Loader active inline />
) : (
<CancelSubmitButtons
marginbottom='0'
onClick={this.props.handleChangeNotEditable()}
disabled={isSubmitting}
/>
)}
</FixedNav>
<HeaderStyled size='large'>
Write a New {this.props.postType}
</HeaderStyled>
<Grid stackable columns={2}>
<Grid.Column>
<Form.Field>
<label>{authorNameLabel}</label>
<Field
component={InputField}
name='customAuthor'
placeholder={
authorNamePlaceholder
}
/>
</Form.Field>
</Grid.Column>
<Grid.Column>
{this.props.postType !==
'Project' && (
<Form.Field>
<label>{postSectionLabel}</label>
<Field
component='select'
name='section'
placeholder='Select a Section'
>
{semanticTypes.map(
({ key, text, value }) => (
<option
key={key}
value={value}
>
{text}
</option>
)
)}
</Field>
</Form.Field>
)}
</Grid.Column>
</Grid>
<Grid stackable columns={2}>
{this.props.postType === 'Project' &&
projTypeSelect && (
<Grid.Column>
<Field
component='select'
name='projectType'
placeholder='Select a project type'
>
{projTypeSelect.map(
({ key, text, value }) => (
<option
key={key}
value={value}
>
{text}
</option>
)
)}
</Field>
</Grid.Column>
)}
{this.props.postType === 'Project' &&
projectAddressesSelect && (
<Grid.Column>
<Field
component='select'
name='projectAddress'
placeholder='Select a project address'
>
{projectAddressesSelect.map(
({ key, text, value }) => (
<option
key={key}
value={value}
>
{text}
</option>
)
)}
</Field>
</Grid.Column>
)}
</Grid>
{this.props.postType === 'Event' && (
<Grid stackable columns={2}>
<Grid.Column>
<Form.Field>
<label>Event Address</label>
<Field
name='address'
component={InputField}
placeholder='Provide an Event Address'
/>
</Form.Field>
</Grid.Column>
<Grid.Column>
<Grid stackable columns={2}>
<Grid.Column>
<Form.Field>
<label>Start Time</label>
<DatePicker
value={this.state.startTime}
onChange={
this
.handleChangeDatePickerStart
}
showTimeSelect
timeFormat='h:mmaa'
timeIntervals={30}
dateFormat='MMMM d, yyyy h:mm aa'
timeCaption='time'
/>
</Form.Field>
{this.state.startTimeError && (
<Error>
Please specify an event
start time
</Error>
)}
</Grid.Column>
<Grid.Column>
<Form.Field>
<label>End Time</label>
<DatePicker
value={this.state.endTime}
onChange={
this
.handleChangeDatePickerEnd
}
showTimeSelect
timeFormat='h:mmaa'
timeIntervals={15}
dateFormat='MMMM d, yyyy h:mm aa'
timeCaption='time'
/>
</Form.Field>
{this.state.endTimeError && (
<Error>
Please specify event end
time{' '}
</Error>
)}
</Grid.Column>
</Grid>
</Grid.Column>
</Grid>
)}
{(this.props.postType === 'Classified' ||
this.props.postType === 'Event') && (
<Grid stackable columns={2}>
<Grid.Column>
<Form.Field>
<label>Contact email</label>
<Field
name='email'
component={InputField}
placeholder='Contact Email (optional)'
/>
</Form.Field>
</Grid.Column>
<Grid.Column>
<Form.Field>
<label>Contact Phone</label>
<Field
name='phone'
component={InputField}
placeholder='Contact Phone (optional)'
/>
</Form.Field>
</Grid.Column>
</Grid>
)}
{this.props.postType === 'Event' && (
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
<label>Tickets:</label>
<Field
name='tickets'
component={InputField}
placeholder='place to buy tickets'
/>
</Form.Field>
</Grid.Column>
</Grid>
)}
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
<label>{postTitleLabel}</label>
<Field
component={InputField}
name='name'
placeholder={postTitlePlaceholder}
/>
</Form.Field>
</Grid.Column>
</Grid>
{this.props.postType !== 'Classified' && (
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
<label>
{postDescriptionLabel}
</label>
<Field
name='description'
component={InputField}
placeholder={
postDescriptionPlaceholder
}
/>
<StyledMessage
visible
warning
content={descriptionMessage}
/>
</Form.Field>
</Grid.Column>
</Grid>
)}
<Grid stackable columns={2}>
<Grid.Column>
<Header as='h3'>Cover Image</Header>
<Form.Field>
<Dropzone
className={'dropzoneStyle'}
onDrop={this.handleDrop}
accept='image/*'
>
Upload Cover Image
</Dropzone>
{this.props.postType === 'Blog' && (
<StyledMessage
visible
warning
content={coverImageMessage}
/>
)}
{imagePrev && (
<CoverImageContainer>
<ImageBackground
imageHeight='220px'
image={imagePrev}
/>
<RemoveButton
onClick={() =>
this.setState({
imagePrev: '',
imageFile: '',
image: ''
})
}
/>
</CoverImageContainer>
)}
</Form.Field>
</Grid.Column>
<Grid.Column>
<Header as='h3'>Video</Header>
<Form.Field>
<Input
name='fullVideo'
label='https://'
placeholder='Video URL'
icon={<Icon name='video' />}
onChange={this.handleChange}
autoComplete='fullVideo'
value={fullVideo}
/>
<StyledMessage
visible
warning
content={videoMessage}
/>
</Form.Field>
{fullVideo ? (
<Card>
<VideoContainer
video={video}
videoSource={videoSource}
/>
</Card>
) : null}
</Grid.Column>
</Grid>
<Grid stackable columns={1}>
<Grid.Column>
<Header as='h3'>
Gallery Images
</Header>
<hr />
<Form.Field>
<Dropzone
className={'dropzoneStyle'}
onDrop={this.handleDropGallery}
accept='image/*'
>
Upload Gallery Images
</Dropzone>
<br />
{Boolean(
this.state.files.length
) && (
<SortableList
axis='xy'
items={this.state.files}
loadingImage={
this.state.loadingImage
}
onSortEnd={this.onSortEnd}
deleteGalImage={
this.deleteGalImage
}
changeCaption={
this.changeCaption
}
/>
)}
</Form.Field>
</Grid.Column>
</Grid>
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
{this.state.loading || loading ? (
<Loader active inline />
) : (
<ButtonsFloat>
<CancelSubmitButtons
marginbottom='0'
onClick={this.props.handleChangeNotEditable()}
disabled={isSubmitting}
/>
</ButtonsFloat>
)}
<Header as='h3'>Content</Header>
<hr />
<CKeditor
activeClass='p10'
id='postEditor'
content={this.state.content}
events={{
blur: this.onBlur,
afterPaste: this.afterPaste,
change: this.onChange
}}
/>
</Form.Field>
{this.state.contentError && (
<Error>
Content cannot be empty
</Error>
)}
</Grid.Column>
</Grid>
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
<label>Tags</label>
<Field
component={InputField}
name='tags'
placeholder='Please Provide Some Tags'
/>
</Form.Field>
{status && status.msg && (
<Message
negative
header='Ooops!'
content={status.msg}
/>
)}
</Grid.Column>
</Grid>
<Grid stackable columns={1}>
<Grid.Column>
<Form.Field>
{this.state.loading || loading ? (
<Loader active inline />
) : (
<CancelSubmitButtons
marginbottom='0'
onClick={this.props.handleChangeNotEditable()}
disabled={isSubmitting}
/>
)}
</Form.Field>
</Grid.Column>
</Grid>
</StyledPostForm>
)
}}
/>
</>
)
}}
</Query>
)
}}
</Query>
)
}}
</Query>
)}
</Mutation>
)
}
}
const Error = styled.p`
&&& {
align-self: center;
color: #e74c3c;
}
`
const FixedNav = styled.div`
&&& {
position: fixed !important;
@media only screen and (min-width: 815px) {
// background: rgba(0, 0, 0, 0.87);
border-bottom-right-radius: 4px;
padding: 7px;
top: 0px;
left: 600px;
z-index: 102 !important;
max-width: 220px;
}
@media (max-width: 814px) {
top: 10px;
right: 10px;
}
}
`
const CoverImageContainer = styled(Card)`
&&& {
position: relative;
}
`
const HeaderStyled = styled(Header)`
&&& {
margin-top: 0;
@media (max-width: 814px) {
padding: 28px 14px 14px;
}
}
`
const ButtonsFloat = styled.div`
&&& {
float: right;
position: relative;
}
`
const StyledMessage = styled(Message)`
&&& {
font-size: 12px;
}
`
export default withUser(withRouter(PostForm))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment