From 798967100bc74bda5c9e13e6e23d4d2035049355 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Wed, 8 Nov 2023 10:34:58 +0100 Subject: [PATCH 1/9] 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) => { <p>{modelError}</p> </div> )} + <div className="blog-container"> + <AddBlogPost /> + <BlogPosts /> + </div> </div> <ChangePassword close={() => 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 <div className="add-blog-post-container">Add blog post</div>; +}; + +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 ( + <div className="blog-post"> + <div> + <div className="blog-post-title"> + {post.title} + </div> + <div className="blog-post-date"> + {dateToDisplay} + </div> + </div> + <div className="blog-post-action-buttons"> + <button className="animated-button blog-post-button" type="button" onClick={onClickEdit}>Edit</button> + <button className="animated-button blog-post-button" type="button" onClick={onClickDelete}>Delete</button> + </div> + </div> + ); +}; + +const BlogPosts = () => { + const [blogPosts, setBlogPosts] = useState([]); + + useEffect(() => { + (async () => { + const blogPostsArray = await getAllBlogPostsByAuthor(); + setBlogPosts(blogPostsArray); + })(); + }, [setBlogPosts]); + + console.log(blogPosts); + return ( + <div className="blog-posts-container"> + <div className="blog-posts-title">Your blog posts</div> + {blogPosts.map((post) => <BlogPost post={post} key={post.id} />)} + </div> + ); +}; + +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 <weronika.ciesielska@lunarlogic.io> Date: Fri, 10 Nov 2023 14:46:45 +0100 Subject: [PATCH 2/9] 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 ( <div className="blog-posts-container"> <div className="blog-posts-title">Your blog posts</div> - {blogPosts.map((post) => <BlogPost post={post} key={post.id} />)} + {blogPosts && blogPosts.map((post) => <BlogPost post={post} onEdit={editBlogPost} onDelete={deleteBlogPost} key={post.id} />)} </div> ); }; 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 <weronika.ciesielska@lunarlogic.io> Date: Fri, 17 Nov 2023 12:01:51 +0100 Subject: [PATCH 3/9] 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 ( + <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; 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 ( - <div className="uploading-message-container"> - <h3>Uploading File...</h3> - </div> - ); - } - - if (uploadingFileError) { - return ( - <div id="uploading-error-container" className="uploading-message-container"> - <h3>{uploadingFileError} Please read <a href={guideURL} target="_blank" rel="noopener noreferrer">this guide</a> for uploading data.</h3> - <button - type="button" - onClick={clearError} - >Try Again - </button> - </div> - ); - } - return ( <> {componentsToRender.map((component) => ( - <div id={component.id} key={component.id}> - <p>{component.name}</p> - <p id="file-selected"> - {component.file ? component.file.name : ''} - </p> - {component.file ? ( - <button - id="upload-button" - className="custom-file-upload" - type="button" - onClick={component.uploadFile} - > - 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=".csv" - onChange={(e) => component.selectFile(e.target.files[0]) && clearSuccessMessage()} - /> - Select File - </label> - </> - )} - </div> + <FileInput component={component} onResetFiles={() => component.selectFile()} guideURL={guideURL} /> ))} </> ); From 03dc2050892cf8543849b5c2cbfb6d9aec819ac2 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Fri, 17 Nov 2023 12:09:58 +0100 Subject: [PATCH 4/9] 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 <div className="add-blog-post-container">Add blog post</div>; +import BlogPostForm from '../blog-post-form'; +import './style.scss'; + +const AddBlogPost = (props) => { + const { createBlogPost } = props; + + return ( + <BlogPostForm onSubmit={createBlogPost} /> + ); }; 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 ( + <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} /> + </div> + </label> + <div className="image-input-container"> + <FileInput component={uploadImageComponent} onResetFiles={resetImage} fileFormat="image/png, image/jpg, image/jpeg, image/pjpeg" /> + </div> + <label htmlFor="body" className="input-label"> + Enter blog post + <div className="input-container"> + <textarea name="body" onChange={handleInputChange} /> + </div> + </label> + <button type="submit" className="blog-form-button animated-button" onClick={handleSubmit}>Submit</button> + </form> + </div> + ); +}; + +export default BlogPostForm; diff --git a/src/screens/admin/components/blog-post-form/index.js b/src/screens/admin/components/blog-post-form/index.js new file mode 100644 index 00000000..e2476aed --- /dev/null +++ b/src/screens/admin/components/blog-post-form/index.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; + +import BlogPostForm from './component'; + +const mapStateToProps = (state) => { + return {}; +}; + +const mapDispatchToProps = (dispatch) => { + return {}; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(BlogPostForm); diff --git a/src/screens/admin/components/blog-post-form/style.scss b/src/screens/admin/components/blog-post-form/style.scss new file mode 100644 index 00000000..f8d2b44b --- /dev/null +++ b/src/screens/admin/components/blog-post-form/style.scss @@ -0,0 +1,61 @@ +.blog-form-button { + width: 114px; + margin: 19px 67px 0 0; + padding: 4px 0; + border-radius: 6px; + background-color: #c9cdd9; + margin-top: 20px; + font-size: 18px; + font-weight: 600; + line-height: 1.8; + color: #2f303a; + margin-left: auto; + margin-right: auto; +} + +.blog-posts-form-title { + font-size: 18px; + font-weight: 600; + color: #2f303a; + margin-bottom: 14px; +} + +.input-container { + + margin: 10px 0; + + input, + textarea { + 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; + } + + textarea { + resize: vertical; + min-height: 150px; + + } +} + +.input-label { + margin-bottom: 10px; +} + +.image-input-container { + margin: 10px 0 18px; + + label { + font-size: 13.3333px; + padding: 6px; + border-radius: 6px; + } +} \ No newline at end of file diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index cb8f522f..c36b6628 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -44,10 +44,21 @@ const BlogPosts = (props) => { getAllBlogPostsByAuthor(); }, [getAllBlogPostsByAuthor]); + // Sort posts from the newest to the oldest + const sortedBlogPosts = blogPosts.sort((a, b) => { + const dateA = new Date(a.date_created); + const dateB = new Date(b.date_created); + + return dateB - dateA; + }); + return ( <div className="blog-posts-container"> <div className="blog-posts-title">Your blog posts</div> - {blogPosts && blogPosts.map((post) => <BlogPost post={post} onEdit={editBlogPost} onDelete={deleteBlogPost} key={post.id} />)} + {sortedBlogPosts.length > 0 + && sortedBlogPosts.map( + (post) => <BlogPost post={post} onEdit={editBlogPost} onDelete={deleteBlogPost} key={post.id} />, + )} </div> ); }; diff --git a/src/screens/admin/style.scss b/src/screens/admin/style.scss index 3bf69815..41fa8b2a 100644 --- a/src/screens/admin/style.scss +++ b/src/screens/admin/style.scss @@ -121,4 +121,5 @@ .blog-posts-container { margin-left: 20px; overflow: scroll; + max-height: 500px; } \ No newline at end of file diff --git a/src/services/blog.js b/src/services/blog.js index d93efc37..2ca16a0b 100644 --- a/src/services/blog.js +++ b/src/services/blog.js @@ -6,15 +6,29 @@ import { const SUBROUTE = 'blog'; -export const createBlogPost = async () => {}; +export const createBlogPost = async (fields) => { + const url = `${global.API_URL}/${SUBROUTE}/create`; + const token = getAuthTokenFromStorage(); + try { + const { data: response } = await axios.post(url, fields, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + const { data } = response; + + return data.blogPost; + } catch (error) { + console.error(error); throw error; + } +}; 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; diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index ad5c4b89..b7ae1769 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -5,6 +5,24 @@ export const ActionTypes = { SET_BLOG_POSTS_BY_USER_DATA: 'SET_BLOG_POSTS_BY_USER_DATA', EDIT_BLOG_POST: 'EDIT_BLOG_POST', DELETE_BLOG_POST: 'DELETE_BLOG_POST', + CREATE_BLOG_POST: 'CREATE_BLOG_POST', +}; + +export const createBlogPost = (fields) => { + return async (dispatch) => { + try { + const createdBlogPost = await BlogService.createBlogPost(fields); + dispatch({ type: ActionTypes.CREATE_BLOG_POST, payload: createdBlogPost }); + } catch (error) { + dispatch({ + type: ActionTypes.API_ERROR, + payload: { + action: 'CREATE BLOG POST', + error, + }, + }); + } + }; }; export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {}) => { diff --git a/src/state/actions/index.js b/src/state/actions/index.js index 8578b9d0..dab15c0b 100644 --- a/src/state/actions/index.js +++ b/src/state/actions/index.js @@ -37,6 +37,7 @@ import { import { ActionTypes as blogActionTypes, + createBlogPost, getAllBlogPostsByAuthor, editBlogPost, deleteBlogPost, @@ -54,6 +55,7 @@ export { clearCustomPredictionError, clearData, clearSelections, + createBlogPost, deleteBlogPost, editBlogPost, getAggregateLocationData, diff --git a/src/state/reducers/blog.js b/src/state/reducers/blog.js index aaf2dd8c..6e8245ec 100644 --- a/src/state/reducers/blog.js +++ b/src/state/reducers/blog.js @@ -6,6 +6,11 @@ const initialState = { const BlogReducer = (state = initialState, action) => { switch (action.type) { + case ActionTypes.CREATE_BLOG_POST: { + const updatedBlogPostsByUser = state.blogPostsByUser.concat([action.payload]); + return { ...state, blogPostsByUser: updatedBlogPostsByUser }; + } + case ActionTypes.SET_BLOG_POSTS_BY_USER_DATA: return { ...state, blogPostsByUser: action.payload }; diff --git a/src/style.scss b/src/style.scss index b4331932..87e15ff8 100644 --- a/src/style.scss +++ b/src/style.scss @@ -40,6 +40,10 @@ body img { border-radius: 0px; } +body input, textarea { + font-family: 'Inter'; +} + .container { width: auto; height: auto; From 4c510f8311d2e446ea9d4588d70172b441812ed5 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Tue, 21 Nov 2023 15:52:27 +0100 Subject: [PATCH 5/9] feat: add edit form for blog posts --- .../components/add-blog-post/component.js | 2 +- .../components/blog-post-form/component.js | 45 +++++++++++++++---- .../admin/components/blog-post-form/index.js | 12 +---- .../components/blog-post-form/style.scss | 20 +++++++++ .../admin/components/blog-posts/component.js | 24 +++++----- .../components/edit-blog-post/component.js | 37 +++++++++++++++ .../admin/components/edit-blog-post/index.js | 3 ++ .../components/edit-blog-post/style.scss | 16 +++++++ .../admin/components/file-upload/component.js | 8 +++- src/state/actions/blog.js | 1 + src/state/reducers/blog.js | 6 ++- 11 files changed, 142 insertions(+), 32 deletions(-) create mode 100644 src/screens/admin/components/edit-blog-post/component.js create mode 100644 src/screens/admin/components/edit-blog-post/index.js create mode 100644 src/screens/admin/components/edit-blog-post/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 42d30602..9c483cd6 100644 --- a/src/screens/admin/components/add-blog-post/component.js +++ b/src/screens/admin/components/add-blog-post/component.js @@ -7,7 +7,7 @@ const AddBlogPost = (props) => { const { createBlogPost } = props; return ( - <BlogPostForm onSubmit={createBlogPost} /> + <BlogPostForm onSubmit={createBlogPost} formTitle="Create blog post" formType="create" /> ); }; diff --git a/src/screens/admin/components/blog-post-form/component.js b/src/screens/admin/components/blog-post-form/component.js index 4c38d236..7e50c9b7 100644 --- a/src/screens/admin/components/blog-post-form/component.js +++ b/src/screens/admin/components/blog-post-form/component.js @@ -5,9 +5,11 @@ import { FileInput } from '../../../../components/input-components'; import './style.scss'; const BlogPostForm = (props) => { - const { onSubmit, formTitle } = props; + const { + onSubmit, formTitle, formValues, formType, + } = props; - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState(formValues || { title: '', body: '', image: null, @@ -43,8 +45,10 @@ const BlogPostForm = (props) => { const uploadImageComponent = { file: formData.image, - id: 'blog-post-image', - name: 'Upload image for your blog post', + id: `${formType}-blog-post-image`, + name: formType === 'edit' + ? 'Change image for your blog post' + : 'Upload image for your blog post', selectFile: handleFileChange, }; @@ -62,19 +66,44 @@ const BlogPostForm = (props) => { <label htmlFor="title" className="input-label"> Title <div className="input-container"> - <input name="title" type="text" placeholder="Title" onChange={handleInputChange} /> + <input + name="title" + type="text" + placeholder="Title" + onChange={handleInputChange} + value={formData.title} + /> </div> </label> <div className="image-input-container"> - <FileInput component={uploadImageComponent} onResetFiles={resetImage} fileFormat="image/png, image/jpg, image/jpeg, image/pjpeg" /> + <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} /> + <textarea + name="body" + onChange={handleInputChange} + value={formData.body} + /> </div> </label> - <button type="submit" className="blog-form-button animated-button" onClick={handleSubmit}>Submit</button> + <button + type="submit" + className="blog-form-button animated-button" + onClick={handleSubmit} + > + Submit + </button> </form> </div> ); diff --git a/src/screens/admin/components/blog-post-form/index.js b/src/screens/admin/components/blog-post-form/index.js index e2476aed..1d648a3a 100644 --- a/src/screens/admin/components/blog-post-form/index.js +++ b/src/screens/admin/components/blog-post-form/index.js @@ -1,13 +1,3 @@ -import { connect } from 'react-redux'; - import BlogPostForm from './component'; -const mapStateToProps = (state) => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return {}; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(BlogPostForm); +export default BlogPostForm; diff --git a/src/screens/admin/components/blog-post-form/style.scss b/src/screens/admin/components/blog-post-form/style.scss index f8d2b44b..096c002f 100644 --- a/src/screens/admin/components/blog-post-form/style.scss +++ b/src/screens/admin/components/blog-post-form/style.scss @@ -52,10 +52,30 @@ .image-input-container { margin: 10px 0 18px; + display: flex; + justify-content: space-between; label { font-size: 13.3333px; padding: 6px; border-radius: 6px; } +} + +.image-preview-container { + width: 50%; + height: 150px; + overflow: hidden; + position: relative; + + img { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + } } \ No newline at end of file diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index c36b6628..1116eca9 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -1,18 +1,12 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import EditBlogPost from '../edit-blog-post'; import './style.scss'; -const BlogPost = ({ post, onEdit, onDelete }) => { +const BlogPost = ({ post, onClickEdit, onDelete }) => { const date = new Date(post.date_created); const dateToDisplay = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; - const onClickEdit = () => { - onEdit( - post.id, - { title: 'New title 40' }, - ); - }; - const onClickDelete = () => { onDelete(post.id); }; @@ -40,6 +34,14 @@ const BlogPosts = (props) => { blogPosts, getAllBlogPostsByAuthor, editBlogPost, deleteBlogPost, } = props; + const [selectedBlogPost, setSelectedBlogPost] = useState({ title: '', body: '', image: null }); + const [showEditForm, setShowEditForm] = useState(false); + + const openEditForm = (post) => { + setShowEditForm(true); + setSelectedBlogPost(post); + }; + useEffect(() => { getAllBlogPostsByAuthor(); }, [getAllBlogPostsByAuthor]); @@ -57,8 +59,10 @@ const BlogPosts = (props) => { <div className="blog-posts-title">Your blog posts</div> {sortedBlogPosts.length > 0 && sortedBlogPosts.map( - (post) => <BlogPost post={post} onEdit={editBlogPost} onDelete={deleteBlogPost} key={post.id} />, + (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={deleteBlogPost} key={post.id} />, )} + <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={editBlogPost} /> + </div> ); }; diff --git a/src/screens/admin/components/edit-blog-post/component.js b/src/screens/admin/components/edit-blog-post/component.js new file mode 100644 index 00000000..60a175e9 --- /dev/null +++ b/src/screens/admin/components/edit-blog-post/component.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import Modal from 'react-modal'; +import BlogPostForm from '../blog-post-form'; + +import './style.scss'; + +const EditBlogPost = (props) => { + const { + isOpen, setIsOpen, post, onSubmit, + } = props; + const handleClose = () => setIsOpen(false); + const handleOpen = () => setIsOpen(true); + + const handleSubmit = (data) => { onSubmit(post.id, data); handleClose(); }; + + return ( + <Modal isOpen={isOpen} + onAfterOpen={handleOpen} + onRequestClose={handleClose} + className="blog-post-edit-modal" + ariaHideApp={false} + > + <BlogPostForm onSubmit={handleSubmit} + formTitle="Edit your blog post" + formValues={{ + title: post.title, + body: post.body, + image: post.image, + }} + formType="edit" + /> + </Modal> + ); +}; + +export default EditBlogPost; diff --git a/src/screens/admin/components/edit-blog-post/index.js b/src/screens/admin/components/edit-blog-post/index.js new file mode 100644 index 00000000..9e525327 --- /dev/null +++ b/src/screens/admin/components/edit-blog-post/index.js @@ -0,0 +1,3 @@ +import EditBlogPost from './component'; + +export default EditBlogPost; diff --git a/src/screens/admin/components/edit-blog-post/style.scss b/src/screens/admin/components/edit-blog-post/style.scss new file mode 100644 index 00000000..d94cf7e2 --- /dev/null +++ b/src/screens/admin/components/edit-blog-post/style.scss @@ -0,0 +1,16 @@ +.blog-post-edit-modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 55vw; + height: 70vh; + padding: 50px; + border-radius: 20px; + background-color: #ffffff; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + + .add-blog-post-container { + width: auto; + } +} \ No newline at end of file diff --git a/src/screens/admin/components/file-upload/component.js b/src/screens/admin/components/file-upload/component.js index 0df4838e..5f43e299 100644 --- a/src/screens/admin/components/file-upload/component.js +++ b/src/screens/admin/components/file-upload/component.js @@ -37,7 +37,13 @@ const FileUpload = (props) => { return ( <> {componentsToRender.map((component) => ( - <FileInput component={component} onResetFiles={() => component.selectFile()} guideURL={guideURL} /> + <FileInput + component={component} + onResetFiles={() => component.selectFile()} + guideURL={guideURL} + key={component.id} + + /> ))} </> ); diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index b7ae1769..6c8cb6da 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -47,6 +47,7 @@ export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {} export const editBlogPost = (id, fields) => { return async (dispatch) => { try { + console.log('id', id, 'fields', fields); const editedBlogPost = await BlogService.editBlogPost(id, fields); dispatch({ type: ActionTypes.EDIT_BLOG_POST, payload: editedBlogPost }); } catch (error) { diff --git a/src/state/reducers/blog.js b/src/state/reducers/blog.js index 6e8245ec..2e2b9331 100644 --- a/src/state/reducers/blog.js +++ b/src/state/reducers/blog.js @@ -15,7 +15,11 @@ const BlogReducer = (state = initialState, action) => { return { ...state, blogPostsByUser: action.payload }; case ActionTypes.EDIT_BLOG_POST: { - const updatedBlogPosts = state.blogPostsByUser.map((post) => (post._id === action.payload._id ? action.payload : post)); + const updatedBlogPosts = state.blogPostsByUser.map( + (post) => (post._id === action.payload._id + ? action.payload + : post), + ); return { ...state, blogPostsByUser: updatedBlogPosts }; } From 37e581455d588e95cd05a92ec4cdb75eced7e6a8 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Thu, 23 Nov 2023 16:11:15 +0100 Subject: [PATCH 6/9] feat: add error handling to blog post forms --- .../admin/components/add-blog-post/index.js | 4 ++-- .../components/blog-post-form/component.js | 23 +++++++++++++++---- .../admin/components/blog-post-form/index.js | 18 ++++++++++++++- .../components/blog-post-form/style.scss | 11 +++++++++ .../admin/components/blog-posts/component.js | 11 +++++++-- .../admin/components/blog-posts/index.js | 4 ++-- .../components/edit-blog-post/component.js | 4 +--- .../components/edit-blog-post/style.scss | 1 + src/state/actions/blog.js | 11 ++++++--- src/state/reducers/blog.js | 7 ++++++ 10 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/screens/admin/components/add-blog-post/index.js b/src/screens/admin/components/add-blog-post/index.js index 625e9257..02d6e67b 100644 --- a/src/screens/admin/components/add-blog-post/index.js +++ b/src/screens/admin/components/add-blog-post/index.js @@ -9,8 +9,8 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - createBlogPost: (fields) => { - dispatch(createBlogPost(fields)); + createBlogPost: (fields, onSuccess) => { + dispatch(createBlogPost(fields, onSuccess)); }, }; }; diff --git a/src/screens/admin/components/blog-post-form/component.js b/src/screens/admin/components/blog-post-form/component.js index 7e50c9b7..381c8de7 100644 --- a/src/screens/admin/components/blog-post-form/component.js +++ b/src/screens/admin/components/blog-post-form/component.js @@ -6,7 +6,7 @@ import './style.scss'; const BlogPostForm = (props) => { const { - onSubmit, formTitle, formValues, formType, + onSubmit, formTitle, formValues, formType, error, } = props; const [formData, setFormData] = useState(formValues || { @@ -39,8 +39,15 @@ const BlogPostForm = (props) => { if (formData.image) { data.append('image', formData.image); } - - await onSubmit(data); + if (formType === 'create') { + await onSubmit(data, () => setFormData({ + title: '', + body: '', + image: null, + })); + } else { + onSubmit(data); + } }; const uploadImageComponent = { @@ -57,6 +64,10 @@ const BlogPostForm = (props) => { 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"> @@ -72,6 +83,7 @@ const BlogPostForm = (props) => { placeholder="Title" onChange={handleInputChange} value={formData.title} + required /> </div> </label> @@ -94,16 +106,19 @@ const BlogPostForm = (props) => { name="body" onChange={handleInputChange} value={formData.body} + required /> </div> </label> <button type="submit" - className="blog-form-button animated-button" + className={`blog-form-button ${isButtonDisabled ? '' : 'animated-button'}`} onClick={handleSubmit} + disabled={isButtonDisabled} > Submit </button> + <div className="blog-form-error">{shouldErrorDisplay && error.message}</div> </form> </div> ); diff --git a/src/screens/admin/components/blog-post-form/index.js b/src/screens/admin/components/blog-post-form/index.js index 1d648a3a..741a461e 100644 --- a/src/screens/admin/components/blog-post-form/index.js +++ b/src/screens/admin/components/blog-post-form/index.js @@ -1,3 +1,19 @@ +import { connect } from 'react-redux'; + import BlogPostForm from './component'; -export default BlogPostForm; +const mapStateToProps = (state) => { + const { + blog: { + error, + }, + } = state; + + return { error }; +}; + +const mapDispatchToProps = (dispatch) => { + return {}; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(BlogPostForm); diff --git a/src/screens/admin/components/blog-post-form/style.scss b/src/screens/admin/components/blog-post-form/style.scss index 096c002f..7f75180f 100644 --- a/src/screens/admin/components/blog-post-form/style.scss +++ b/src/screens/admin/components/blog-post-form/style.scss @@ -11,6 +11,17 @@ color: #2f303a; margin-left: auto; margin-right: auto; + + &:disabled { + border: none; + opacity: 0.5; + cursor: not-allowed; + } +} + +.blog-form-error { + margin-top: 10px; + color: red } .blog-posts-form-title { diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index 1116eca9..8f8b2d04 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -34,7 +34,9 @@ const BlogPosts = (props) => { blogPosts, getAllBlogPostsByAuthor, editBlogPost, deleteBlogPost, } = props; - const [selectedBlogPost, setSelectedBlogPost] = useState({ title: '', body: '', image: null }); + const [selectedBlogPost, setSelectedBlogPost] = useState({ + id: null, title: '', body: '', image: null, + }); const [showEditForm, setShowEditForm] = useState(false); const openEditForm = (post) => { @@ -54,6 +56,11 @@ const BlogPosts = (props) => { return dateB - dateA; }); + const handleFormSubmit = async (formData) => { + const closeModal = () => { setShowEditForm(false); }; + await editBlogPost(selectedBlogPost.id, formData, closeModal); + }; + return ( <div className="blog-posts-container"> <div className="blog-posts-title">Your blog posts</div> @@ -61,7 +68,7 @@ const BlogPosts = (props) => { && sortedBlogPosts.map( (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={deleteBlogPost} key={post.id} />, )} - <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={editBlogPost} /> + <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={handleFormSubmit} /> </div> ); diff --git a/src/screens/admin/components/blog-posts/index.js b/src/screens/admin/components/blog-posts/index.js index 13240202..2d66e067 100644 --- a/src/screens/admin/components/blog-posts/index.js +++ b/src/screens/admin/components/blog-posts/index.js @@ -20,8 +20,8 @@ const mapDispatchToProps = (dispatch) => { getAllBlogPostsByAuthor: (onSuccess, onError) => { dispatch(getAllBlogPostsByAuthor(onSuccess, onError)); }, - editBlogPost: (id, fields) => { - dispatch(editBlogPost(id, fields)); + editBlogPost: (id, fields, onSuccess) => { + dispatch(editBlogPost(id, fields, onSuccess)); }, deleteBlogPost: (id) => { dispatch(deleteBlogPost(id)); diff --git a/src/screens/admin/components/edit-blog-post/component.js b/src/screens/admin/components/edit-blog-post/component.js index 60a175e9..4e6b9a64 100644 --- a/src/screens/admin/components/edit-blog-post/component.js +++ b/src/screens/admin/components/edit-blog-post/component.js @@ -12,8 +12,6 @@ const EditBlogPost = (props) => { const handleClose = () => setIsOpen(false); const handleOpen = () => setIsOpen(true); - const handleSubmit = (data) => { onSubmit(post.id, data); handleClose(); }; - return ( <Modal isOpen={isOpen} onAfterOpen={handleOpen} @@ -21,7 +19,7 @@ const EditBlogPost = (props) => { className="blog-post-edit-modal" ariaHideApp={false} > - <BlogPostForm onSubmit={handleSubmit} + <BlogPostForm onSubmit={onSubmit} formTitle="Edit your blog post" formValues={{ title: post.title, diff --git a/src/screens/admin/components/edit-blog-post/style.scss b/src/screens/admin/components/edit-blog-post/style.scss index d94cf7e2..8e03dc9d 100644 --- a/src/screens/admin/components/edit-blog-post/style.scss +++ b/src/screens/admin/components/edit-blog-post/style.scss @@ -9,6 +9,7 @@ border-radius: 20px; background-color: #ffffff; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + overflow: scroll; .add-blog-post-container { width: auto; diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index 6c8cb6da..b82de719 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -2,17 +2,20 @@ import { blog as BlogService } from '../../services'; export const ActionTypes = { API_ERROR: 'API_ERROR', + CLEAR_API_ERROR: 'CLEAR_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', CREATE_BLOG_POST: 'CREATE_BLOG_POST', }; -export const createBlogPost = (fields) => { +export const createBlogPost = (fields, onSuccess) => { return async (dispatch) => { try { + dispatch({ type: ActionTypes.CLEAR_API_ERROR }); const createdBlogPost = await BlogService.createBlogPost(fields); dispatch({ type: ActionTypes.CREATE_BLOG_POST, payload: createdBlogPost }); + onSuccess(); } catch (error) { dispatch({ type: ActionTypes.API_ERROR, @@ -44,12 +47,14 @@ export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {} }; }; -export const editBlogPost = (id, fields) => { +export const editBlogPost = (id, fields, onSuccess) => { return async (dispatch) => { try { - console.log('id', id, 'fields', fields); + dispatch({ type: ActionTypes.CLEAR_API_ERROR }); + const editedBlogPost = await BlogService.editBlogPost(id, fields); dispatch({ type: ActionTypes.EDIT_BLOG_POST, payload: editedBlogPost }); + onSuccess(); } catch (error) { dispatch({ type: ActionTypes.API_ERROR, diff --git a/src/state/reducers/blog.js b/src/state/reducers/blog.js index 2e2b9331..cbfdb55c 100644 --- a/src/state/reducers/blog.js +++ b/src/state/reducers/blog.js @@ -2,6 +2,7 @@ import { ActionTypes } from '../actions'; const initialState = { blogPostsByUser: [], + error: null, }; const BlogReducer = (state = initialState, action) => { @@ -28,6 +29,12 @@ const BlogReducer = (state = initialState, action) => { return { ...state, blogPostsByUser: filteredBlogPosts }; } + case ActionTypes.API_ERROR: + return { ...state, error: { message: action.payload.error.response?.data?.error, action: action.payload.action } }; + + case ActionTypes.CLEAR_API_ERROR: + return { ...state, error: null }; + default: return state; } From 74c941e929f3e145676228c808f569d415b23c39 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Thu, 23 Nov 2023 16:44:59 +0100 Subject: [PATCH 7/9] feat: add delete modal to blog post list --- .../admin/components/blog-posts/component.js | 39 ++++++++++++++++++- .../admin/components/blog-posts/index.js | 4 +- .../admin/components/blog-posts/style.scss | 31 +++++++++++++++ src/state/actions/blog.js | 3 +- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index 8f8b2d04..a7f151b1 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import Modal from 'react-modal'; import EditBlogPost from '../edit-blog-post'; import './style.scss'; @@ -29,6 +30,30 @@ const BlogPost = ({ post, onClickEdit, onDelete }) => { ); }; +const DeleteModal = ({ + handleDelete, isOpen, setIsOpen, +}) => { + const handleClose = () => setIsOpen(false); + const handleOpen = () => setIsOpen(true); + + return ( + <Modal isOpen={isOpen} + onAfterOpen={handleOpen} + onRequestClose={handleClose} + className="delete-blog-post-modal" + ariaHideApp={false} + > + <div> + Are you sure you want to delete? + <div className="delete-blog-post-buttons"> + <button type="button" className="blog-post-button animated-button" onClick={handleDelete}>Yes</button> + <button type="button" className="blog-post-button animated-button" onClick={handleClose}>No</button> + </div> + </div> + </Modal> + ); +}; + const BlogPosts = (props) => { const { blogPosts, getAllBlogPostsByAuthor, editBlogPost, deleteBlogPost, @@ -38,12 +63,18 @@ const BlogPosts = (props) => { id: null, title: '', body: '', image: null, }); const [showEditForm, setShowEditForm] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); const openEditForm = (post) => { setShowEditForm(true); setSelectedBlogPost(post); }; + const openDeleteModal = (post) => { + setShowDeleteModal(true); + setSelectedBlogPost(post); + }; + useEffect(() => { getAllBlogPostsByAuthor(); }, [getAllBlogPostsByAuthor]); @@ -61,14 +92,20 @@ const BlogPosts = (props) => { await editBlogPost(selectedBlogPost.id, formData, closeModal); }; + const handleDeleteBlogPost = async () => { + const closeModal = () => setShowDeleteModal(false); + await deleteBlogPost(selectedBlogPost.id, closeModal); + }; + return ( <div className="blog-posts-container"> <div className="blog-posts-title">Your blog posts</div> {sortedBlogPosts.length > 0 && sortedBlogPosts.map( - (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={deleteBlogPost} key={post.id} />, + (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={() => openDeleteModal(post)} key={post.id} />, )} <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={handleFormSubmit} /> + <DeleteModal handleDelete={handleDeleteBlogPost} isOpen={showDeleteModal} setIsOpen={setShowDeleteModal} /> </div> ); diff --git a/src/screens/admin/components/blog-posts/index.js b/src/screens/admin/components/blog-posts/index.js index 2d66e067..ec49528b 100644 --- a/src/screens/admin/components/blog-posts/index.js +++ b/src/screens/admin/components/blog-posts/index.js @@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch) => { editBlogPost: (id, fields, onSuccess) => { dispatch(editBlogPost(id, fields, onSuccess)); }, - deleteBlogPost: (id) => { - dispatch(deleteBlogPost(id)); + deleteBlogPost: (id, onSuccess) => { + dispatch(deleteBlogPost(id, onSuccess)); }, }; }; diff --git a/src/screens/admin/components/blog-posts/style.scss b/src/screens/admin/components/blog-posts/style.scss index 7368a2df..d12b1c2b 100644 --- a/src/screens/admin/components/blog-posts/style.scss +++ b/src/screens/admin/components/blog-posts/style.scss @@ -46,3 +46,34 @@ color: #2f303a; margin-bottom: 14px; } + +.delete-blog-post-modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20vw; + height: 10vh; + padding: 50px; + border-radius: 20px; + background-color: #ffffff; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + overflow: scroll; + display: flex; + justify-content: center; + align-items: center; +} + +.delete-blog-post-buttons { + display: flex; + width: 100%; + justify-content: space-between; + box-sizing: border-box; + padding: 0 50px; + margin-top: 20px; + + .blog-post-button { + padding-left: 20px; + padding-right: 20px; + } +} \ No newline at end of file diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index b82de719..c89d3366 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -67,12 +67,13 @@ export const editBlogPost = (id, fields, onSuccess) => { }; }; -export const deleteBlogPost = (id) => { +export const deleteBlogPost = (id, onSuccess) => { return async (dispatch) => { try { const response = await BlogService.deleteBlogPost(id); if (response.status === 200) { dispatch({ type: ActionTypes.DELETE_BLOG_POST, payload: { _id: id } }); + onSuccess(); } } catch (error) { dispatch({ From d5287d3d6311c0f537cc96ed8eba2e661ed79d36 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Fri, 24 Nov 2023 12:48:02 +0100 Subject: [PATCH 8/9] feat: add post title to delete modal --- .gitignore | 1 + src/screens/admin/components/blog-posts/component.js | 6 +++--- src/screens/admin/components/blog-posts/style.scss | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 7db80cb6..3698ee4a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ yarn-error.log .env .eslintcache +.vscode diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index a7f151b1..55f6077f 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -31,7 +31,7 @@ const BlogPost = ({ post, onClickEdit, onDelete }) => { }; const DeleteModal = ({ - handleDelete, isOpen, setIsOpen, + handleDelete, isOpen, setIsOpen, title, }) => { const handleClose = () => setIsOpen(false); const handleOpen = () => setIsOpen(true); @@ -44,7 +44,7 @@ const DeleteModal = ({ ariaHideApp={false} > <div> - Are you sure you want to delete? + Are you sure you want to delete <span className="delete-blog-post-title">{`"${title}" `}</span>? <div className="delete-blog-post-buttons"> <button type="button" className="blog-post-button animated-button" onClick={handleDelete}>Yes</button> <button type="button" className="blog-post-button animated-button" onClick={handleClose}>No</button> @@ -105,7 +105,7 @@ const BlogPosts = (props) => { (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={() => openDeleteModal(post)} key={post.id} />, )} <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={handleFormSubmit} /> - <DeleteModal handleDelete={handleDeleteBlogPost} isOpen={showDeleteModal} setIsOpen={setShowDeleteModal} /> + <DeleteModal handleDelete={handleDeleteBlogPost} isOpen={showDeleteModal} setIsOpen={setShowDeleteModal} title={selectedBlogPost.title} /> </div> ); diff --git a/src/screens/admin/components/blog-posts/style.scss b/src/screens/admin/components/blog-posts/style.scss index d12b1c2b..93da0cb1 100644 --- a/src/screens/admin/components/blog-posts/style.scss +++ b/src/screens/admin/components/blog-posts/style.scss @@ -76,4 +76,8 @@ padding-left: 20px; padding-right: 20px; } +} + +.delete-blog-post-title { + font-style: italic; } \ No newline at end of file From 7229df88d50b5292d576d1797369dd9c22cd3351 Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska <weronika.ciesielska@lunarlogic.io> Date: Fri, 24 Nov 2023 13:58:18 +0100 Subject: [PATCH 9/9] style: adjustments after self-review --- src/screens/admin/components/blog-posts/component.js | 6 +++--- src/state/actions/blog.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/screens/admin/components/blog-posts/component.js b/src/screens/admin/components/blog-posts/component.js index 55f6077f..c7343823 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -101,9 +101,9 @@ const BlogPosts = (props) => { <div className="blog-posts-container"> <div className="blog-posts-title">Your blog posts</div> {sortedBlogPosts.length > 0 - && sortedBlogPosts.map( - (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={() => openDeleteModal(post)} key={post.id} />, - )} + ? sortedBlogPosts.map( + (post) => <BlogPost post={post} onClickEdit={() => openEditForm(post)} onDelete={() => openDeleteModal(post)} key={post.id} />, + ) : <div>You haven't written any blog posts yet!</div>} <EditBlogPost isOpen={showEditForm} setIsOpen={setShowEditForm} post={selectedBlogPost} onSubmit={handleFormSubmit} /> <DeleteModal handleDelete={handleDeleteBlogPost} isOpen={showDeleteModal} setIsOpen={setShowDeleteModal} title={selectedBlogPost.title} /> diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index c89d3366..be9f7a33 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -9,7 +9,7 @@ export const ActionTypes = { CREATE_BLOG_POST: 'CREATE_BLOG_POST', }; -export const createBlogPost = (fields, onSuccess) => { +export const createBlogPost = (fields, onSuccess = () => {}) => { return async (dispatch) => { try { dispatch({ type: ActionTypes.CLEAR_API_ERROR }); @@ -47,7 +47,7 @@ export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {} }; }; -export const editBlogPost = (id, fields, onSuccess) => { +export const editBlogPost = (id, fields, onSuccess = () => {}) => { return async (dispatch) => { try { dispatch({ type: ActionTypes.CLEAR_API_ERROR }); @@ -67,7 +67,7 @@ export const editBlogPost = (id, fields, onSuccess) => { }; }; -export const deleteBlogPost = (id, onSuccess) => { +export const deleteBlogPost = (id, onSuccess = () => {}) => { return async (dispatch) => { try { const response = await BlogService.deleteBlogPost(id);