From 798967100bc74bda5c9e13e6e23d4d2035049355 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Wed, 8 Nov 2023 10:34:58 +0100 Subject: [PATCH 01/25] feat: add list of blog posts to admin panel --- src/screens/admin/component.js | 6 ++ .../components/add-blog-post/component.js | 7 ++ .../admin/components/add-blog-post/index.js | 13 +++ .../admin/components/add-blog-post/style.scss | 0 .../admin/components/blog-posts/component.js | 58 +++++++++++++ .../admin/components/blog-posts/index.js | 13 +++ .../admin/components/blog-posts/style.scss | 48 ++++++++++ src/screens/admin/components/index.js | 4 + src/screens/admin/style.scss | 29 +++++-- src/services/blog.js | 87 +++++++++++++++++++ src/services/index.js | 2 + 11 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 src/screens/admin/components/add-blog-post/component.js create mode 100644 src/screens/admin/components/add-blog-post/index.js create mode 100644 src/screens/admin/components/add-blog-post/style.scss create mode 100644 src/screens/admin/components/blog-posts/component.js create mode 100644 src/screens/admin/components/blog-posts/index.js create mode 100644 src/screens/admin/components/blog-posts/style.scss create mode 100644 src/services/blog.js diff --git a/src/screens/admin/component.js b/src/screens/admin/component.js index 75fe1beb..d6fb24d5 100644 --- a/src/screens/admin/component.js +++ b/src/screens/admin/component.js @@ -1,7 +1,9 @@ import React, { useState } from 'react'; import { + AddBlogPost, AddUser, + BlogPosts, ChangePassword, FileUpload, Login, @@ -78,6 +80,10 @@ const Admin = (props) => {

{modelError}

)} +
+ + +
setChangePasswordVisible(false)} diff --git a/src/screens/admin/components/add-blog-post/component.js b/src/screens/admin/components/add-blog-post/component.js new file mode 100644 index 00000000..e45a7e77 --- /dev/null +++ b/src/screens/admin/components/add-blog-post/component.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const AddBlogPost = () => { + return
Add blog post
; +}; + +export default AddBlogPost; diff --git a/src/screens/admin/components/add-blog-post/index.js b/src/screens/admin/components/add-blog-post/index.js new file mode 100644 index 00000000..8239409a --- /dev/null +++ b/src/screens/admin/components/add-blog-post/index.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; + +import AddBlogPost from './component'; + +const mapStateToProps = (state) => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return {}; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AddBlogPost); diff --git a/src/screens/admin/components/add-blog-post/style.scss b/src/screens/admin/components/add-blog-post/style.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js new file mode 100644 index 00000000..49a04f81 --- /dev/null +++ b/src/screens/admin/components/blog-posts/component.js @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react'; +import { deleteBlogPost, editBlogPost, getAllBlogPostsByAuthor } from '../../../../services/blog'; + +import './style.scss'; + +const BlogPost = ({ post }) => { + const date = new Date(post.date_created); + const dateToDisplay = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; + + const onClickEdit = () => { + editBlogPost( + post.id, + { title: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ulla.' }, + ); + }; + + const onClickDelete = () => { + deleteBlogPost(post.id); + }; + + return ( +
+
+
+ {post.title} +
+
+ {dateToDisplay} +
+
+
+ + +
+
+ ); +}; + +const BlogPosts = () => { + const [blogPosts, setBlogPosts] = useState([]); + + useEffect(() => { + (async () => { + const blogPostsArray = await getAllBlogPostsByAuthor(); + setBlogPosts(blogPostsArray); + })(); + }, [setBlogPosts]); + + console.log(blogPosts); + return ( +
+
Your blog posts
+ {blogPosts.map((post) => )} +
+ ); +}; + +export default BlogPosts; diff --git a/src/screens/admin/components/blog-posts/index.js b/src/screens/admin/components/blog-posts/index.js new file mode 100644 index 00000000..9416c92b --- /dev/null +++ b/src/screens/admin/components/blog-posts/index.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; + +import BlogPosts from './component'; + +const mapStateToProps = (state) => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return {}; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(BlogPosts); diff --git a/src/screens/admin/components/blog-posts/style.scss b/src/screens/admin/components/blog-posts/style.scss new file mode 100644 index 00000000..d35b86ee --- /dev/null +++ b/src/screens/admin/components/blog-posts/style.scss @@ -0,0 +1,48 @@ +.blog-post { + width: 100%; + box-sizing: border-box; + margin-bottom: 8px; + padding: 5px 16px; + border: none; + border-radius: 10px; + box-shadow: 0 6px 40px 0 rgba(0, 0, 0, 0.04); + background-color: #ffffff; + font-size: 15px; + line-height: 1.51; + color: #62697e; + display: flex; + justify-content: space-between; + align-items: center; + + &-title { + font-weight: 600; + margin-right: 6px; + width: 250px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &-date { + font-style: italic; + } + + &-action-buttons { + border-left: 1px solid #c9cdd9; + padding-left: 6px; + } + + &-button { + padding: 6px; + border-radius: 6px; + background-color: #c9cdd9; + margin-left: 6px; + } +} + +.blog-posts-title { + font-size: 18px; + font-weight: 600; + color: #2f303a; + margin-bottom: 14px; +} \ No newline at end of file diff --git a/src/screens/admin/components/index.js b/src/screens/admin/components/index.js index 955c412a..1a14608a 100644 --- a/src/screens/admin/components/index.js +++ b/src/screens/admin/components/index.js @@ -1,11 +1,15 @@ +import AddBlogPost from './add-blog-post/component'; import AddUser from './add-user'; +import BlogPosts from './blog-posts/component'; import ChangePassword from './change-password'; import FileUpload from './file-upload'; import Login from './login'; import Users from './users'; export { + AddBlogPost, AddUser, + BlogPosts, ChangePassword, FileUpload, Login, diff --git a/src/screens/admin/style.scss b/src/screens/admin/style.scss index 3e50a22e..3bf69815 100644 --- a/src/screens/admin/style.scss +++ b/src/screens/admin/style.scss @@ -38,7 +38,7 @@ } } - #dashboard-container{ + #dashboard-container { padding: 37px; border-radius: 26px; box-shadow: 0 35px 90px -10px rgba(0, 0, 0, 0.06); @@ -64,7 +64,7 @@ width: 400px; } - * + * { + *+* { margin-top: 15px; } } @@ -77,7 +77,8 @@ overflow: scroll; margin-left: 20px; - #users-container, #add-users { + #users-container, + #add-users { flex: 1 1 auto; padding: 23px; border-radius: 22px; @@ -91,7 +92,7 @@ } #rerun-button { - margin: 40px 346px 0 349px; + margin: 40px 346px 40px 349px; padding: 8px 34px 7px 35px; border-radius: 6px; background-color: #2f303a; @@ -100,6 +101,24 @@ line-height: 1.8; color: #ffffff; white-space: nowrap; - } + } } } + +.blog-container { + display: flex; +} + +.add-blog-post-container, +.blog-posts-container { + flex: 1 1 auto; + padding: 23px; + border-radius: 22px; + background-color: #f8f9ff; + width: 50%; +} + +.blog-posts-container { + margin-left: 20px; + overflow: scroll; +} \ No newline at end of file diff --git a/src/services/blog.js b/src/services/blog.js new file mode 100644 index 00000000..a25d6e98 --- /dev/null +++ b/src/services/blog.js @@ -0,0 +1,87 @@ +import axios from 'axios'; + +import { + getAuthTokenFromStorage, getUserIdFromStorage, +} from '../utils'; + +const SUBROUTE = 'blog'; + +export const createBlogPost = async () => {}; + +export const getAllBlogPosts = async () => { + const url = `${global.API_URL}/${SUBROUTE}`; + + try { + console.log('req'); + const { data: response } = await axios.get(url); + console.log('res'); + + const { data } = response; + + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const getAllBlogPostsByAuthor = async () => { + const userId = getUserIdFromStorage(); + const url = `${global.API_URL}/${SUBROUTE}/user/${userId}`; + const token = getAuthTokenFromStorage(); + + try { + const { data: response } = await axios.get(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + const { data } = response; + + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const editBlogPost = async (id, fields) => { + const url = `${global.API_URL}/${SUBROUTE}/${id}`; + const token = getAuthTokenFromStorage(); + + try { + const { data: response } = await axios.put(url, fields, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + const { data } = response; + + return data; + } catch (error) { + console.error(error); + throw error; + } +}; + +export const deleteBlogPost = async (id) => { + const url = `${global.API_URL}/${SUBROUTE}/${id}`; + const token = getAuthTokenFromStorage(); + + try { + const { data: response } = await axios.delete(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + const { data } = response; + + return data; + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/src/services/index.js b/src/services/index.js index befa82eb..97a82ca1 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,9 +1,11 @@ import * as admin from './admin'; import * as api from './api'; +import * as blog from './blog'; import * as user from './user'; export { admin, api, + blog, user, }; From 74d98d1604773a77b19fd369626f1ac400ee7647 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Fri, 10 Nov 2023 14:46:45 +0100 Subject: [PATCH 02/25] feat: use redux for blog posts --- .../admin/components/blog-posts/component.js | 27 ++++---- .../admin/components/blog-posts/index.js | 23 ++++++- .../admin/components/blog-posts/style.scss | 2 +- src/screens/admin/components/index.js | 4 +- src/services/blog.js | 4 +- src/state/actions/blog.js | 63 +++++++++++++++++++ src/state/actions/index.js | 11 ++++ src/state/reducers/blog.js | 27 ++++++++ src/state/reducers/index.js | 2 + 9 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 src/state/actions/blog.js create mode 100644 src/state/reducers/blog.js diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index 49a04f81..cb8f522f 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -1,21 +1,20 @@ -import React, { useEffect, useState } from 'react'; -import { deleteBlogPost, editBlogPost, getAllBlogPostsByAuthor } from '../../../../services/blog'; +import React, { useEffect } from 'react'; import './style.scss'; -const BlogPost = ({ post }) => { +const BlogPost = ({ post, onEdit, onDelete }) => { const date = new Date(post.date_created); const dateToDisplay = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; const onClickEdit = () => { - editBlogPost( + onEdit( post.id, - { title: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ulla.' }, + { title: 'New title 40' }, ); }; const onClickDelete = () => { - deleteBlogPost(post.id); + onDelete(post.id); }; return ( @@ -36,21 +35,19 @@ const BlogPost = ({ post }) => { ); }; -const BlogPosts = () => { - const [blogPosts, setBlogPosts] = useState([]); +const BlogPosts = (props) => { + const { + blogPosts, getAllBlogPostsByAuthor, editBlogPost, deleteBlogPost, + } = props; useEffect(() => { - (async () => { - const blogPostsArray = await getAllBlogPostsByAuthor(); - setBlogPosts(blogPostsArray); - })(); - }, [setBlogPosts]); + getAllBlogPostsByAuthor(); + }, [getAllBlogPostsByAuthor]); - console.log(blogPosts); return (
Your blog posts
- {blogPosts.map((post) => )} + {blogPosts && blogPosts.map((post) => )}
); }; diff --git a/src/screens/admin/components/blog-posts/index.js b/src/screens/admin/components/blog-posts/index.js index 9416c92b..13240202 100644 --- a/src/screens/admin/components/blog-posts/index.js +++ b/src/screens/admin/components/blog-posts/index.js @@ -1,13 +1,32 @@ import { connect } from 'react-redux'; import BlogPosts from './component'; +import { deleteBlogPost, editBlogPost, getAllBlogPostsByAuthor } from '../../../../state/actions'; const mapStateToProps = (state) => { - return {}; + const { + blog: { + blogPostsByUser: blogPosts, + }, + } = state; + + return { + blogPosts, + }; }; const mapDispatchToProps = (dispatch) => { - return {}; + return { + getAllBlogPostsByAuthor: (onSuccess, onError) => { + dispatch(getAllBlogPostsByAuthor(onSuccess, onError)); + }, + editBlogPost: (id, fields) => { + dispatch(editBlogPost(id, fields)); + }, + deleteBlogPost: (id) => { + dispatch(deleteBlogPost(id)); + }, + }; }; export default connect(mapStateToProps, mapDispatchToProps)(BlogPosts); diff --git a/src/screens/admin/components/blog-posts/style.scss b/src/screens/admin/components/blog-posts/style.scss index d35b86ee..7368a2df 100644 --- a/src/screens/admin/components/blog-posts/style.scss +++ b/src/screens/admin/components/blog-posts/style.scss @@ -45,4 +45,4 @@ font-weight: 600; color: #2f303a; margin-bottom: 14px; -} \ No newline at end of file +} diff --git a/src/screens/admin/components/index.js b/src/screens/admin/components/index.js index 1a14608a..a3e30039 100644 --- a/src/screens/admin/components/index.js +++ b/src/screens/admin/components/index.js @@ -1,6 +1,6 @@ -import AddBlogPost from './add-blog-post/component'; +import AddBlogPost from './add-blog-post'; import AddUser from './add-user'; -import BlogPosts from './blog-posts/component'; +import BlogPosts from './blog-posts'; import ChangePassword from './change-password'; import FileUpload from './file-upload'; import Login from './login'; diff --git a/src/services/blog.js b/src/services/blog.js index a25d6e98..d93efc37 100644 --- a/src/services/blog.js +++ b/src/services/blog.js @@ -77,9 +77,7 @@ export const deleteBlogPost = async (id) => { }, }); - const { data } = response; - - return data; + return response; } catch (error) { console.error(error); throw error; diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js new file mode 100644 index 00000000..ad5c4b89 --- /dev/null +++ b/src/state/actions/blog.js @@ -0,0 +1,63 @@ +import { blog as BlogService } from '../../services'; + +export const ActionTypes = { + API_ERROR: 'API_ERROR', + SET_BLOG_POSTS_BY_USER_DATA: 'SET_BLOG_POSTS_BY_USER_DATA', + EDIT_BLOG_POST: 'EDIT_BLOG_POST', + DELETE_BLOG_POST: 'DELETE_BLOG_POST', +}; + +export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {}) => { + return async (dispatch) => { + try { + const blogPosts = await BlogService.getAllBlogPostsByAuthor(); + dispatch({ type: ActionTypes.SET_BLOG_POSTS_BY_USER_DATA, payload: blogPosts }); + onSuccess(); + } catch (error) { + dispatch({ + type: ActionTypes.API_ERROR, + payload: { + action: 'GET ALL BLOG POSTS', + error, + }, + }); + onError(error); + } + }; +}; + +export const editBlogPost = (id, fields) => { + return async (dispatch) => { + try { + const editedBlogPost = await BlogService.editBlogPost(id, fields); + dispatch({ type: ActionTypes.EDIT_BLOG_POST, payload: editedBlogPost }); + } catch (error) { + dispatch({ + type: ActionTypes.API_ERROR, + payload: { + action: 'EDIT BLOG POST', + error, + }, + }); + } + }; +}; + +export const deleteBlogPost = (id) => { + return async (dispatch) => { + try { + const response = await BlogService.deleteBlogPost(id); + if (response.status === 200) { + dispatch({ type: ActionTypes.DELETE_BLOG_POST, payload: { _id: id } }); + } + } catch (error) { + dispatch({ + type: ActionTypes.API_ERROR, + payload: { + action: 'DELETE BLOG POST', + error, + }, + }); + } + }; +}; diff --git a/src/state/actions/index.js b/src/state/actions/index.js index 9d88071c..8578b9d0 100644 --- a/src/state/actions/index.js +++ b/src/state/actions/index.js @@ -35,10 +35,18 @@ import { runCustomPrediction, } from './data'; +import { + ActionTypes as blogActionTypes, + getAllBlogPostsByAuthor, + editBlogPost, + deleteBlogPost, +} from './blog'; + const ActionTypes = { ...dataActionTypes, ...selectionActionTypes, ...userActionTypes, + ...blogActionTypes, }; export { @@ -46,9 +54,12 @@ export { clearCustomPredictionError, clearData, clearSelections, + deleteBlogPost, + editBlogPost, getAggregateLocationData, getAggregateStateData, getAggregateYearData, + getAllBlogPostsByAuthor, getAvailableStates, getAvailableSublocations, getAvailableYears, diff --git a/src/state/reducers/blog.js b/src/state/reducers/blog.js new file mode 100644 index 00000000..aaf2dd8c --- /dev/null +++ b/src/state/reducers/blog.js @@ -0,0 +1,27 @@ +import { ActionTypes } from '../actions'; + +const initialState = { + blogPostsByUser: [], +}; + +const BlogReducer = (state = initialState, action) => { + switch (action.type) { + case ActionTypes.SET_BLOG_POSTS_BY_USER_DATA: + return { ...state, blogPostsByUser: action.payload }; + + case ActionTypes.EDIT_BLOG_POST: { + const updatedBlogPosts = state.blogPostsByUser.map((post) => (post._id === action.payload._id ? action.payload : post)); + return { ...state, blogPostsByUser: updatedBlogPosts }; + } + + case ActionTypes.DELETE_BLOG_POST: { + const filteredBlogPosts = state.blogPostsByUser.filter((post) => post._id !== action.payload._id); + return { ...state, blogPostsByUser: filteredBlogPosts }; + } + + default: + return state; + } +}; + +export default BlogReducer; diff --git a/src/state/reducers/index.js b/src/state/reducers/index.js index 3ca302a8..da0e76bd 100644 --- a/src/state/reducers/index.js +++ b/src/state/reducers/index.js @@ -1,11 +1,13 @@ import { combineReducers } from 'redux'; +import BlogReducer from './blog'; import ErrorReducer from './error'; import DataReducer from './data'; import SelectionsReducer from './selections'; import UserReducer from './user'; const rootReducer = combineReducers({ + blog: BlogReducer, data: DataReducer, error: ErrorReducer, selections: SelectionsReducer, From d84b8647dbf440ebf4f79fe448b095cde62a9567 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Fri, 17 Nov 2023 12:01:51 +0100 Subject: [PATCH 03/25] feat: create FileInput component using UploadFile component --- .../input-components/file-input/index.js | 121 ++++++++++++++++++ .../input-components/file-input}/style.scss | 0 src/components/input-components/index.js | 2 + .../admin/components/file-upload/component.js | 108 +--------------- 4 files changed, 128 insertions(+), 103 deletions(-) create mode 100644 src/components/input-components/file-input/index.js rename src/{screens/admin/components/file-upload => components/input-components/file-input}/style.scss (100%) diff --git a/src/components/input-components/file-input/index.js b/src/components/input-components/file-input/index.js new file mode 100644 index 00000000..26e8d2b0 --- /dev/null +++ b/src/components/input-components/file-input/index.js @@ -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 ( +
+

Uploading File...

+
+ ); + } + + if (uploadingFileError) { + return ( +
+ { + guideURL + ?

{uploadingFileError} Please read this guide for uploading data.

+ :

{uploadingFileError}

+ } + +
+ ); + } + + /** + * @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 ( +
+

{component.name}

+

+ {component.file ? component.file.name : ''} +

+ {component.file && component.uploadFile ? ( + + ) : ( + <> + {successMessage[component.id] && ( +

{successMessage[component.id]}

+ )} + + + )} +
+ ); +}; + +export default FileInput; diff --git a/src/screens/admin/components/file-upload/style.scss b/src/components/input-components/file-input/style.scss similarity index 100% rename from src/screens/admin/components/file-upload/style.scss rename to src/components/input-components/file-input/style.scss diff --git a/src/components/input-components/index.js b/src/components/input-components/index.js index 24598b34..2eb9c75f 100644 --- a/src/components/input-components/index.js +++ b/src/components/input-components/index.js @@ -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, }; diff --git a/src/screens/admin/components/file-upload/component.js b/src/screens/admin/components/file-upload/component.js index edad2028..0df4838e 100644 --- a/src/screens/admin/components/file-upload/component.js +++ b/src/screens/admin/components/file-upload/component.js @@ -1,13 +1,12 @@ import React, { useState } from 'react'; +import { FileInput } from '../../../../components/input-components'; import { uploadCountySpotCsv, uploadRangerDistrictSpotCsv, uploadSurvey123UnsummarizedCsv, } from '../../../../services/admin'; -import './style.scss'; - const FileUpload = (props) => { const { guideURL } = props; @@ -15,127 +14,30 @@ const FileUpload = (props) => { const [rdSpotFile, setRdSpotFile] = useState(); const [unsummarizedFile, setUnsummarizedFile] = useState(); - const [isUploadingFile, setIsUploadingFile] = useState(false); - const [uploadingFileError, setUploadingFileError] = useState(''); - const [successMessage, setSuccessMessage] = useState({}); - - const clearSuccessMessage = () => setSuccessMessage({}); - - const clearError = () => { - setUploadingFileError(''); - setCountySpotFile(); - setRdSpotFile(); - setUnsummarizedFile(); - setIsUploadingFile(false); - setSuccessMessage({}); - }; - - /** - * @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'); - - if (badColumnNames) setUploadingFileError('Incorrect column names. Please upload a different CSV.'); - else if (badRequest) setUploadingFileError(`Bad request: ${strippedError}`); - else setUploadingFileError(strippedError || data?.error.toString() || 'We encountered an error. Please try again.'); - } finally { - setIsUploadingFile(false); - } - }; - const componentsToRender = [{ file: countySpotFile, id: 'county-spot', name: 'Upload File for County Spot Data', selectFile: setCountySpotFile, - uploadFile: () => uploadFile(uploadCountySpotCsv, countySpotFile, setCountySpotFile, 'county-spot'), + uploadFile: uploadCountySpotCsv, }, { file: rdSpotFile, id: 'rd-spot', name: 'Upload File for Ranger District Spot Data', selectFile: setRdSpotFile, - uploadFile: () => uploadFile(uploadRangerDistrictSpotCsv, rdSpotFile, setRdSpotFile, 'rd-spot'), + uploadFile: uploadRangerDistrictSpotCsv, }, { file: unsummarizedFile, id: 'unsummarized', name: 'Upload File for Survey123 Unsummarized Data', selectFile: setUnsummarizedFile, - uploadFile: () => uploadFile(uploadSurvey123UnsummarizedCsv, unsummarizedFile, setUnsummarizedFile, 'unsummarized'), + uploadFile: uploadSurvey123UnsummarizedCsv, }]; - if (isUploadingFile) { - return ( -
-

Uploading File...

-
- ); - } - - if (uploadingFileError) { - return ( -
-

{uploadingFileError} Please read this guide for uploading data.

- -
- ); - } - return ( <> {componentsToRender.map((component) => ( -
-

{component.name}

-

- {component.file ? component.file.name : ''} -

- {component.file ? ( - - ) : ( - <> - {successMessage[component.id] && ( -

{successMessage[component.id]}

- )} - - - )} -
+ component.selectFile()} guideURL={guideURL} /> ))} ); From 03dc2050892cf8543849b5c2cbfb6d9aec819ac2 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Fri, 17 Nov 2023 12:09:58 +0100 Subject: [PATCH 04/25] feat: add BlogPostForm component and AddBlogPost form --- .../components/add-blog-post/component.js | 11 ++- .../admin/components/add-blog-post/index.js | 7 +- .../components/blog-post-form/component.js | 83 +++++++++++++++++++ .../admin/components/blog-post-form/index.js | 13 +++ .../components/blog-post-form/style.scss | 61 ++++++++++++++ .../admin/components/blog-posts/component.js | 13 ++- src/screens/admin/style.scss | 1 + src/services/blog.js | 20 ++++- src/state/actions/blog.js | 18 ++++ src/state/actions/index.js | 2 + src/state/reducers/blog.js | 5 ++ src/style.scss | 4 + 12 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 src/screens/admin/components/blog-post-form/component.js create mode 100644 src/screens/admin/components/blog-post-form/index.js create mode 100644 src/screens/admin/components/blog-post-form/style.scss diff --git a/src/screens/admin/components/add-blog-post/component.js b/src/screens/admin/components/add-blog-post/component.js index e45a7e77..42d30602 100644 --- a/src/screens/admin/components/add-blog-post/component.js +++ b/src/screens/admin/components/add-blog-post/component.js @@ -1,7 +1,14 @@ import React from 'react'; -const AddBlogPost = () => { - return
Add blog post
; +import BlogPostForm from '../blog-post-form'; +import './style.scss'; + +const AddBlogPost = (props) => { + const { createBlogPost } = props; + + return ( + + ); }; export default AddBlogPost; diff --git a/src/screens/admin/components/add-blog-post/index.js b/src/screens/admin/components/add-blog-post/index.js index 8239409a..625e9257 100644 --- a/src/screens/admin/components/add-blog-post/index.js +++ b/src/screens/admin/components/add-blog-post/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; +import { createBlogPost } from '../../../../state/actions'; import AddBlogPost from './component'; const mapStateToProps = (state) => { @@ -7,7 +8,11 @@ const mapStateToProps = (state) => { }; const mapDispatchToProps = (dispatch) => { - return {}; + return { + createBlogPost: (fields) => { + dispatch(createBlogPost(fields)); + }, + }; }; export default connect(mapStateToProps, mapDispatchToProps)(AddBlogPost); diff --git a/src/screens/admin/components/blog-post-form/component.js b/src/screens/admin/components/blog-post-form/component.js new file mode 100644 index 00000000..4c38d236 --- /dev/null +++ b/src/screens/admin/components/blog-post-form/component.js @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; + +import { FileInput } from '../../../../components/input-components'; + +import './style.scss'; + +const BlogPostForm = (props) => { + const { onSubmit, formTitle } = props; + + const [formData, setFormData] = useState({ + 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); + } + + await onSubmit(data); + }; + + const uploadImageComponent = { + file: formData.image, + id: 'blog-post-image', + name: 'Upload image for your blog post', + selectFile: handleFileChange, + }; + + const resetImage = () => setFormData({ + ...formData, + image: null, + }); + + return ( +
+
+ {formTitle} +
+
+ +
+ +
+