From ce73d51bc351f8c1e9f44a6dec9395b7083f73ca Mon Sep 17 00:00:00 2001 From: Weronika Ciesielska Date: Mon, 27 Nov 2023 15:42:34 +0100 Subject: [PATCH] feat: create blog page and display blog posts --- src/app/component.js | 2 + src/constants/index.js | 1 + .../components/blog-post-form/style.scss | 2 +- .../admin/components/blog-posts/component.js | 14 ++---- src/screens/blog/component.js | 32 ++++++++++++++ .../blog/components/blog-post/component.js | 28 ++++++++++++ .../blog/components/blog-post/index.js | 3 ++ .../blog/components/blog-post/style.scss | 44 +++++++++++++++++++ src/screens/blog/components/index.js | 3 ++ src/screens/blog/index.js | 21 +++++++++ src/screens/blog/style.scss | 14 ++++++ src/screens/index.js | 2 + src/state/actions/blog.js | 18 +++++++- src/state/reducers/blog.js | 4 ++ src/utils/blog.js | 33 ++++++++++++++ src/utils/index.js | 9 ++++ 16 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 src/screens/blog/component.js create mode 100644 src/screens/blog/components/blog-post/component.js create mode 100644 src/screens/blog/components/blog-post/index.js create mode 100644 src/screens/blog/components/blog-post/style.scss create mode 100644 src/screens/blog/components/index.js create mode 100644 src/screens/blog/index.js create mode 100644 src/screens/blog/style.scss create mode 100644 src/utils/blog.js diff --git a/src/app/component.js b/src/app/component.js index 63235a62..08a45685 100644 --- a/src/app/component.js +++ b/src/app/component.js @@ -14,6 +14,7 @@ import { Home, PlayWithModel, Prediction, + Blog, } from '../screens'; import { @@ -120,6 +121,7 @@ const App = (props) => { + diff --git a/src/constants/index.js b/src/constants/index.js index 08c21178..ea160af2 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -71,6 +71,7 @@ const CHART_MODES = { const ROUTES = { ABOUT: '/about', ADMIN: '/admin', + BLOG: '/blog', HOME: '/', RESOURCES: '/resources', PLAY_WITH_MODEL: '/play-with-model', diff --git a/src/screens/admin/components/blog-post-form/style.scss b/src/screens/admin/components/blog-post-form/style.scss index 3c55e610..404cd38a 100644 --- a/src/screens/admin/components/blog-post-form/style.scss +++ b/src/screens/admin/components/blog-post-form/style.scss @@ -87,7 +87,7 @@ transform: translate(-50%, -50%); min-width: 100%; min-height: 100%; - width: auto; + width: inherit; 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 c7343823..5d5165b3 100644 --- a/src/screens/admin/components/blog-posts/component.js +++ b/src/screens/admin/components/blog-posts/component.js @@ -2,12 +2,11 @@ import React, { useEffect, useState } from 'react'; import Modal from 'react-modal'; import EditBlogPost from '../edit-blog-post'; +import { getDateToDisplay, sortBlogPosts } from '../../../../utils'; + import './style.scss'; const BlogPost = ({ post, onClickEdit, onDelete }) => { - const date = new Date(post.date_created); - const dateToDisplay = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; - const onClickDelete = () => { onDelete(post.id); }; @@ -19,7 +18,7 @@ const BlogPost = ({ post, onClickEdit, onDelete }) => { {post.title}
- {dateToDisplay} + {getDateToDisplay(post.date_created)}
@@ -80,12 +79,7 @@ const BlogPosts = (props) => { }, [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; - }); + const sortedBlogPosts = sortBlogPosts(blogPosts); const handleFormSubmit = async (formData) => { const closeModal = () => { setShowEditForm(false); }; diff --git a/src/screens/blog/component.js b/src/screens/blog/component.js new file mode 100644 index 00000000..7df98db3 --- /dev/null +++ b/src/screens/blog/component.js @@ -0,0 +1,32 @@ +import React, { useEffect } from 'react'; + +import BlogPost from './components'; +import { sortBlogPosts } from '../../utils'; + +import './style.scss'; + +const Blog = (props) => { + const { + blogPosts, + getAllBlogPosts, + } = props; + + useEffect(() => { + getAllBlogPosts(); + }, [getAllBlogPosts]); + + const sortedBlogPosts = sortBlogPosts(blogPosts); + + return ( +
+
+

Blog

+
+ {sortedBlogPosts.length > 0 + ? sortedBlogPosts.map((post) => ) + :
There are no blog posts yet
} +
+ ); +}; + +export default Blog; diff --git a/src/screens/blog/components/blog-post/component.js b/src/screens/blog/components/blog-post/component.js new file mode 100644 index 00000000..7adb76f2 --- /dev/null +++ b/src/screens/blog/components/blog-post/component.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { formatPostDates } from '../../../../utils'; + +import './style.scss'; + +const BlogPost = ({ post }) => { + const { + title, + body, + date_created: createdAt, + date_edited: editedAt, + image, + author, + } = post; + + return ( +
+
{title}
+
{`by ${author}`}
+ {image && blog post illustration} +
{body}
+
{formatPostDates(createdAt, editedAt)}
+
+ ); +}; + +export default BlogPost; diff --git a/src/screens/blog/components/blog-post/index.js b/src/screens/blog/components/blog-post/index.js new file mode 100644 index 00000000..c967d424 --- /dev/null +++ b/src/screens/blog/components/blog-post/index.js @@ -0,0 +1,3 @@ +import BlogPost from './component'; + +export default BlogPost; diff --git a/src/screens/blog/components/blog-post/style.scss b/src/screens/blog/components/blog-post/style.scss new file mode 100644 index 00000000..b01e8da2 --- /dev/null +++ b/src/screens/blog/components/blog-post/style.scss @@ -0,0 +1,44 @@ +.blog-page { + &-blog-post { + &-container { + margin: 30px auto; + width: 100%; + box-sizing: border-box; + } + + &-title { + font-size: 24px; + font-weight: 600; + } + + &-author { + margin-bottom: 20px; + margin-top: 6px; + } + + &-picture { + max-width: 100%; + height: auto; + max-height: 500px; + margin: 30px auto 50px; + } + + &-date { + font-style: italic; + font-size: 14px; + margin-top: 30px; + width: 100%; + text-align: right; + } + + &-date, + &-author { + font-style: italic; + color: #73767e; + } + + &-body { + white-space: pre-line; + } + } +} \ No newline at end of file diff --git a/src/screens/blog/components/index.js b/src/screens/blog/components/index.js new file mode 100644 index 00000000..5746af2e --- /dev/null +++ b/src/screens/blog/components/index.js @@ -0,0 +1,3 @@ +import BlogPost from './blog-post'; + +export default BlogPost; diff --git a/src/screens/blog/index.js b/src/screens/blog/index.js new file mode 100644 index 00000000..af79ca1d --- /dev/null +++ b/src/screens/blog/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { getAllBlogPosts } from '../../state/actions/blog'; +import Blog from './component'; + +const mapStateToProps = (state) => { + const { + blog: { + blogPosts, + }, + } = state; + + return { blogPosts }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + getAllBlogPosts: () => dispatch(getAllBlogPosts()), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Blog); diff --git a/src/screens/blog/style.scss b/src/screens/blog/style.scss new file mode 100644 index 00000000..c269d661 --- /dev/null +++ b/src/screens/blog/style.scss @@ -0,0 +1,14 @@ +.blog-page-container { + max-width: 1000px; + margin: auto; + display: flex; + flex-direction: column; + align-items: center; +} + +.blog-page-no-posts { + font-size: 26px; + width: 100%; + text-align: center; + color: #73767e; +} \ No newline at end of file diff --git a/src/screens/index.js b/src/screens/index.js index 8bba250e..0d72b67e 100644 --- a/src/screens/index.js +++ b/src/screens/index.js @@ -1,5 +1,6 @@ import About from './about'; import Admin from './admin'; +import Blog from './blog'; import Resources from './resources'; import TrappingData from './trapping-data'; import Home from './home'; @@ -9,6 +10,7 @@ import Prediction from './prediction'; export { About, Admin, + Blog, Resources, TrappingData, Home, diff --git a/src/state/actions/blog.js b/src/state/actions/blog.js index be9f7a33..86d081dc 100644 --- a/src/state/actions/blog.js +++ b/src/state/actions/blog.js @@ -4,6 +4,7 @@ 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', + GET_ALL_BLOG_POSTS: 'GET_ALL_BLOG_POSTS', EDIT_BLOG_POST: 'EDIT_BLOG_POST', DELETE_BLOG_POST: 'DELETE_BLOG_POST', CREATE_BLOG_POST: 'CREATE_BLOG_POST', @@ -28,6 +29,21 @@ export const createBlogPost = (fields, onSuccess = () => {}) => { }; }; +export const getAllBlogPosts = () => { + return async (dispatch) => { + try { + const blogPosts = await BlogService.getAllBlogPosts(); + dispatch({ type: ActionTypes.GET_ALL_BLOG_POSTS, payload: blogPosts }); + } catch (error) { + dispatch({ + type: ActionTypes.API_ERROR, + payload: 'GET ALL BLOG POSTS', + error, + }); + } + }; +}; + export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {}) => { return async (dispatch) => { try { @@ -38,7 +54,7 @@ export const getAllBlogPostsByAuthor = (onSuccess = () => {}, onError = () => {} dispatch({ type: ActionTypes.API_ERROR, payload: { - action: 'GET ALL BLOG POSTS', + action: 'GET ALL BLOG POSTS BY USER', error, }, }); diff --git a/src/state/reducers/blog.js b/src/state/reducers/blog.js index cbfdb55c..0a9119e8 100644 --- a/src/state/reducers/blog.js +++ b/src/state/reducers/blog.js @@ -1,6 +1,7 @@ import { ActionTypes } from '../actions'; const initialState = { + blogPosts: [], blogPostsByUser: [], error: null, }; @@ -15,6 +16,9 @@ const BlogReducer = (state = initialState, action) => { case ActionTypes.SET_BLOG_POSTS_BY_USER_DATA: return { ...state, blogPostsByUser: action.payload }; + case ActionTypes.GET_ALL_BLOG_POSTS: + return { ...state, blogPosts: action.payload }; + case ActionTypes.EDIT_BLOG_POST: { const updatedBlogPosts = state.blogPostsByUser.map( (post) => (post._id === action.payload._id diff --git a/src/utils/blog.js b/src/utils/blog.js new file mode 100644 index 00000000..2971b01a --- /dev/null +++ b/src/utils/blog.js @@ -0,0 +1,33 @@ +// Sort posts from the newest to the oldest +const sortBlogPosts = (blogPosts) => blogPosts.sort((a, b) => { + const dateA = new Date(a.date_created); + const dateB = new Date(b.date_created); + + return dateB - dateA; +}); + +// Return string with US date format +const getDateToDisplay = (dateToParse) => { + const date = new Date(dateToParse); + return `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; +}; + +// Return string with blog post creation and editing dates +const formatPostDates = (created, updated) => { + const options = { + year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', + }; + const createdDate = new Date(created).toLocaleString('en-US', options); + const updatedDate = new Date(updated).toLocaleString('en-US', options); + + let dateString = `Posted on ${createdDate}`; + if (updated && created !== updated) { + dateString += `, Last updated on ${updatedDate}`; + } + + return dateString; +}; + +export { + sortBlogPosts, getDateToDisplay, formatPostDates, +}; diff --git a/src/utils/index.js b/src/utils/index.js index 95a2c4b1..dec4553e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -22,20 +22,29 @@ import { toQueryParams, } from './network'; +import { + sortBlogPosts, + getDateToDisplay, + formatPostDates, +} from './blog'; + export { downloadCsv, getAuthTokenFromStorage, getChartModeFromStorage, getDataModeFromStorage, + getDateToDisplay, getMapboxRDNameFormat, getStateAbbreviationFromStateName, getStateNameFromAbbreviation, getUserIdFromStorage, + formatPostDates, removeAuthTokenFromStorage, removeUserIdFromStorage, setAuthTokenInStorage, setChartModeInStorage, setDataModeInStorage, setUserIdInStorage, + sortBlogPosts, toQueryParams, };