-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #677 from dali-lab/blog-posts-admin-panel
Blog posts functionality for admin panel
- Loading branch information
Showing
27 changed files
with
975 additions
and
108 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ yarn-error.log | |
.env | ||
|
||
.eslintcache | ||
.vscode |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import './style.scss'; | ||
|
||
const FileInput = (props) => { | ||
const { | ||
guideURL, component, onResetFiles, fileFormat = '.csv', | ||
} = props; | ||
|
||
const [isUploadingFile, setIsUploadingFile] = useState(false); | ||
const [uploadingFileError, setUploadingFileError] = useState(''); | ||
const [successMessage, setSuccessMessage] = useState({}); | ||
|
||
const clearSuccessMessage = () => setSuccessMessage({}); | ||
|
||
const clearError = () => { | ||
setUploadingFileError(''); | ||
onResetFiles(); | ||
setIsUploadingFile(false); | ||
setSuccessMessage({}); | ||
}; | ||
|
||
if (isUploadingFile) { | ||
return ( | ||
<div className="uploading-message-container"> | ||
<h3>Uploading File...</h3> | ||
</div> | ||
); | ||
} | ||
|
||
if (uploadingFileError) { | ||
return ( | ||
<div id="uploading-error-container" className="uploading-message-container"> | ||
{ | ||
guideURL | ||
? <h3>{uploadingFileError} Please read <a href={guideURL} target="_blank" rel="noopener noreferrer">this guide</a> for uploading data.</h3> | ||
: <h3>{uploadingFileError}</h3> | ||
} | ||
<button | ||
type="button" | ||
onClick={clearError} | ||
>Try Again | ||
</button> | ||
</div> | ||
); | ||
} | ||
|
||
/** | ||
* @description uploads given file | ||
* @param {Function} uploadFunction function to upload file | ||
* @param {File} file file object | ||
* @param {Function} clearFile function to clear the file | ||
* @param {String} id file id | ||
*/ | ||
const uploadFile = async (uploadFunction, file, clearFile, id) => { | ||
setIsUploadingFile(true); | ||
|
||
try { | ||
await uploadFunction(file); | ||
clearFile(); | ||
setSuccessMessage({ [id]: 'Successfully uploaded file' }); | ||
setTimeout(clearSuccessMessage, 1000 * 7); | ||
} catch (err) { | ||
const { data, status } = err?.response || {}; | ||
|
||
const strippedError = data?.error.toString().replace('Error: ', ''); | ||
|
||
const badRequest = status === 400; | ||
const badColumnNames = strippedError.includes('missing fields in csv'); | ||
const wrongFileFormat = strippedError.includes('Invalid file type'); | ||
|
||
if (badColumnNames) setUploadingFileError('Incorrect column names. Please upload a different CSV.'); | ||
else if (wrongFileFormat) setUploadingFileError('Invalid file type. Only PNG, JPG, and JPEG files are allowed! Please, choose a different file.'); | ||
else if (badRequest) setUploadingFileError(`Bad request: ${strippedError}`); | ||
else setUploadingFileError(strippedError || data?.error.toString() || 'We encountered an error. Please try again.'); | ||
} finally { | ||
setIsUploadingFile(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<div id={component.id} key={component.id}> | ||
<p>{component.name}</p> | ||
<p id="file-selected"> | ||
{component.file ? component.file.name : ''} | ||
</p> | ||
{component.file && component.uploadFile ? ( | ||
<button | ||
id="upload-button" | ||
className="custom-file-upload" | ||
type="button" | ||
onClick={() => uploadFile( | ||
component.uploadFile, | ||
component.file, | ||
component.selectFile, | ||
component.id, | ||
)} | ||
> | ||
Upload File | ||
</button> | ||
) : ( | ||
<> | ||
{successMessage[component.id] && ( | ||
<p id="success-message">{successMessage[component.id]}</p> | ||
)} | ||
<label htmlFor={`file-upload-${component.id}`} className="custom-file-upload"> | ||
<input | ||
id={`file-upload-${component.id}`} | ||
type="file" | ||
accept={fileFormat} | ||
onChange={(e) => component.selectFile(e.target.files[0]) && clearSuccessMessage()} | ||
/> | ||
Select File | ||
</label> | ||
</> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default FileInput; |
File renamed without changes.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import ChoiceInput from './choice-input'; | ||
import TextInput from './text-input'; | ||
import MultiSelectInput from './multi-select-input'; | ||
import FileInput from './file-input'; | ||
|
||
export { | ||
ChoiceInput, | ||
TextInput, | ||
MultiSelectInput, | ||
FileInput, | ||
}; |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import React from 'react'; | ||
|
||
import BlogPostForm from '../blog-post-form'; | ||
import './style.scss'; | ||
|
||
const AddBlogPost = (props) => { | ||
const { createBlogPost } = props; | ||
|
||
return ( | ||
<BlogPostForm onSubmit={createBlogPost} formTitle="Create blog post" formType="create" /> | ||
); | ||
}; | ||
|
||
export default AddBlogPost; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { connect } from 'react-redux'; | ||
|
||
import { createBlogPost } from '../../../../state/actions'; | ||
import AddBlogPost from './component'; | ||
|
||
const mapStateToProps = (state) => { | ||
return {}; | ||
}; | ||
|
||
const mapDispatchToProps = (dispatch) => { | ||
return { | ||
createBlogPost: (fields, onSuccess) => { | ||
dispatch(createBlogPost(fields, onSuccess)); | ||
}, | ||
}; | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(AddBlogPost); |
Empty file.
127 changes: 127 additions & 0 deletions
127
src/screens/admin/components/blog-post-form/component.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import { FileInput } from '../../../../components/input-components'; | ||
|
||
import './style.scss'; | ||
|
||
const BlogPostForm = (props) => { | ||
const { | ||
onSubmit, formTitle, formValues, formType, error, | ||
} = props; | ||
|
||
const [formData, setFormData] = useState(formValues || { | ||
title: '', | ||
body: '', | ||
image: null, | ||
}); | ||
|
||
const handleInputChange = (ev) => { | ||
return setFormData({ | ||
...formData, | ||
[ev.target.name]: ev.target.value, | ||
}); | ||
}; | ||
|
||
const handleFileChange = (file) => { | ||
setFormData({ | ||
...formData, | ||
image: file, | ||
}); | ||
}; | ||
|
||
const handleSubmit = async (ev) => { | ||
ev.preventDefault(); | ||
|
||
const data = new FormData(); | ||
data.append('title', formData.title); | ||
data.append('body', formData.body); | ||
|
||
if (formData.image) { | ||
data.append('image', formData.image); | ||
} | ||
if (formType === 'create') { | ||
await onSubmit(data, () => setFormData({ | ||
title: '', | ||
body: '', | ||
image: null, | ||
})); | ||
} else { | ||
onSubmit(data); | ||
} | ||
}; | ||
|
||
const uploadImageComponent = { | ||
file: formData.image, | ||
id: `${formType}-blog-post-image`, | ||
name: formType === 'edit' | ||
? 'Change image for your blog post' | ||
: 'Upload image for your blog post', | ||
selectFile: handleFileChange, | ||
}; | ||
|
||
const resetImage = () => setFormData({ | ||
...formData, | ||
image: null, | ||
}); | ||
|
||
const isButtonDisabled = !formData.title.length || !formData.body.length; | ||
|
||
const shouldErrorDisplay = error?.action && error.action.toLowerCase().includes(formType); | ||
|
||
return ( | ||
<div className="add-blog-post-container"> | ||
<div className="blog-posts-form-title"> | ||
{formTitle} | ||
</div> | ||
<form> | ||
<label htmlFor="title" className="input-label"> | ||
Title | ||
<div className="input-container"> | ||
<input | ||
name="title" | ||
type="text" | ||
placeholder="Title" | ||
onChange={handleInputChange} | ||
value={formData.title} | ||
required | ||
/> | ||
</div> | ||
</label> | ||
<div className="image-input-container"> | ||
<FileInput | ||
component={uploadImageComponent} | ||
onResetFiles={resetImage} | ||
fileFormat="image/png, image/jpg, image/jpeg, image/pjpeg" | ||
/> | ||
{typeof formData.image === 'string' && ( | ||
<div className="image-preview-container"> | ||
<img src={formData.image} alt="blog post illustration" /> | ||
</div> | ||
)} | ||
</div> | ||
<label htmlFor="body" className="input-label"> | ||
Enter blog post | ||
<div className="input-container"> | ||
<textarea | ||
name="body" | ||
onChange={handleInputChange} | ||
value={formData.body} | ||
required | ||
/> | ||
</div> | ||
</label> | ||
<button | ||
type="submit" | ||
className={`blog-form-button ${isButtonDisabled ? '' : 'animated-button'}`} | ||
onClick={handleSubmit} | ||
disabled={isButtonDisabled} | ||
> | ||
Submit | ||
</button> | ||
<div className="blog-form-error">{shouldErrorDisplay && error.message}</div> | ||
</form> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BlogPostForm; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { connect } from 'react-redux'; | ||
|
||
import BlogPostForm from './component'; | ||
|
||
const mapStateToProps = (state) => { | ||
const { | ||
blog: { | ||
error, | ||
}, | ||
} = state; | ||
|
||
return { error }; | ||
}; | ||
|
||
const mapDispatchToProps = (dispatch) => { | ||
return {}; | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(BlogPostForm); |
Oops, something went wrong.