(
54 |
60 |
61 | {({ input, meta }) => (
62 |
63 |
64 | {meta.touched && meta.error && (
65 | {meta.error}
66 | )}
67 |
68 | )}
69 |
70 |
71 |
72 |
73 | {({ input, meta }) => (
74 |
75 |
76 | {meta.touched && meta.error && (
77 | {meta.error}
78 | )}
79 |
80 | )}
81 |
82 |
83 |
84 | {!isEmpty(submissionErrors) && (
85 |
86 | {typeof submissionErrors === "object" ? (
87 | Object.entries(submissionErrors).map(([key, value]) => (
88 |
89 | {value}
90 |
91 | ))
92 | ) : (
93 |
94 | {submissionErrors}
95 |
96 | )}
97 |
98 | )}
99 |
100 |
101 |
104 |
111 |
112 |
113 | )}
114 | />
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/client/src/pages/Posts/NewPost.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import { Form, Input, Button, Tag, message } from "antd";
4 | import { Form as FinalForm, Field } from "react-final-form";
5 | import isEmpty from "lodash.isempty";
6 | import { useSelector } from "react-redux";
7 | import TextArea from "antd/lib/input/TextArea";
8 | import { postsAPI } from "./../../api/api";
9 |
10 | export default function NewPost() {
11 | const router = useHistory();
12 | const [initialValues, setInitialValues] = useState({});
13 | const [submissionErrors, setSubmissionErrors] = useState({});
14 | const userState = useSelector((st) => st.user);
15 |
16 | const onSubmit = async (event) => {
17 | try {
18 | await postsAPI.add({
19 | post: { ...event, createdBy: userState.user.id },
20 | });
21 | message.success("Post created successfully");
22 | router.push("/");
23 | } catch (error) {
24 | console.log("Error creating a new post...", error.response ?? error);
25 | if (error.response && error.response.data) {
26 | setSubmissionErrors(error.response.data);
27 | } else setSubmissionErrors({ err: "Post error" });
28 | }
29 | };
30 |
31 | const checkValidation = (values) => {
32 | const errors = {};
33 | if (!values.title?.trim()) {
34 | errors.title = "Please enter the post's title";
35 | }
36 | if (!values.content?.trim()) {
37 | errors.content = "Please enter the post's content";
38 | }
39 | return errors;
40 | };
41 |
42 | return (
43 |
44 |
Create a new post
45 |
(
50 |
56 |
57 | {({ input, meta }) => (
58 |
59 |
60 | {meta.touched && meta.error && (
61 | {meta.error}
62 | )}
63 |
64 | )}
65 |
66 |
67 |
68 |
69 |
70 | {({ input, meta }) => (
71 |
72 |
73 | {meta.touched && meta.error && (
74 | {meta.error}
75 | )}
76 |
77 | )}
78 |
79 |
80 |
81 |
82 |
83 | {({ input, meta }) => (
84 |
85 |
86 | {meta.touched && meta.error && (
87 | {meta.error}
88 | )}
89 |
90 | )}
91 |
92 |
93 |
94 | {!isEmpty(submissionErrors) && (
95 |
96 | {Object.entries(submissionErrors).map(([key, value]) => (
97 |
98 | {value}
99 |
100 | ))}
101 |
102 | )}
103 |
104 |
105 |
108 |
111 |
112 |
113 | )}
114 | />
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/pages/Comments/CommentsMobile.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import defaultUser from "./../../assets/images/default-user.png";
3 | import Avatar from "antd/lib/avatar/avatar";
4 | import "./Comments.scss";
5 | import moment from "moment";
6 | import { EditTwoTone, DeleteTwoTone } from "@ant-design/icons";
7 | import { Form as FinalForm, Field } from "react-final-form";
8 | import { Form, Input, Button, Alert } from "antd";
9 |
10 | export default function CommentsMobile({
11 | comment,
12 | index,
13 | userState,
14 | setDeleteModal,
15 | selectedEditCommentID,
16 | setDeleteSelectedCommentID,
17 | setEditSelectedCommentID,
18 | onSubmit,
19 | }) {
20 | const [initialValues, setInitialValues] = useState({});
21 | const [submissionErrors, setSubmissionErrors] = useState(null);
22 | return (
23 |
24 |
27 |
28 |
29 |
{comment.createdBy.userName}
30 |
31 | {moment(comment.createdAt).fromNow(false)}
32 |
33 |
34 | {comment._id === selectedEditCommentID ? (
35 |
(
39 |
47 |
48 | {({ input, meta }) => (
49 |
50 |
51 |
52 | )}
53 |
54 |
55 |
56 | {submissionErrors && (
57 |
63 | )}
64 |
65 |
66 |
73 |
80 |
81 |
82 | )}
83 | />
84 | ) : (
85 | comment.content
86 | )}
87 |
88 |
89 |
90 | {comment._id !== selectedEditCommentID && (
91 | <>
92 | {comment.createdBy._id === userState.user.id && (
93 |
94 | {
97 | setEditSelectedCommentID(comment._id);
98 | setInitialValues({ ...comment });
99 | }}
100 | />
101 | {
105 | setDeleteSelectedCommentID(comment._id);
106 | setDeleteModal(true);
107 | }}
108 | />
109 |
110 | )}
111 | >
112 | )}
113 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/client/src/pages/Comments/CommentsDesktop.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import defaultUser from "./../../assets/images/default-user.png";
3 | import Avatar from "antd/lib/avatar/avatar";
4 | import "./Comments.scss";
5 | import moment from "moment";
6 | import { EditTwoTone, DeleteTwoTone } from "@ant-design/icons";
7 | import { Form as FinalForm, Field } from "react-final-form";
8 | import { Form, Input, Button, Alert } from "antd";
9 |
10 | export default function CommentsDesktop({
11 | comment,
12 | index,
13 | userState,
14 | setDeleteModal,
15 | selectedEditCommentID,
16 | setDeleteSelectedCommentID,
17 | setEditSelectedCommentID,
18 | onSubmit,
19 | }) {
20 | const [initialValues, setInitialValues] = useState({});
21 | const [submissionErrors, setSubmissionErrors] = useState(null);
22 | return (
23 |
24 |
27 |
32 |
33 |
{comment.createdBy.userName}
34 |
35 | {comment._id === selectedEditCommentID ? (
36 |
(
40 |
48 |
49 | {({ input, meta }) => (
50 |
51 |
52 |
53 | )}
54 |
55 |
56 |
57 | {submissionErrors && (
58 |
64 | )}
65 |
66 |
67 |
74 |
81 |
82 |
83 | )}
84 | />
85 | ) : (
86 | comment.content
87 | )}
88 |
89 |
90 |
91 | {comment._id !== selectedEditCommentID && (
92 | <>
93 | {moment(comment.createdAt).fromNow(false)}
94 | {comment.createdBy._id === userState.user.id && (
95 |
96 | {
99 | setEditSelectedCommentID(comment._id);
100 | setInitialValues({ ...comment });
101 | }}
102 | />
103 | {
107 | setDeleteSelectedCommentID(comment._id);
108 | setDeleteModal(true);
109 | }}
110 | />
111 |
112 | )}
113 | >
114 | )}
115 |
116 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/client/src/pages/Comments/Comments.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import defaultUser from "./../../assets/images/default-user.png";
3 | import Avatar from "antd/lib/avatar/avatar";
4 | import "./Comments.scss";
5 | import moment from "moment";
6 | import { EditTwoTone, DeleteTwoTone } from "@ant-design/icons";
7 | import { useSelector } from "react-redux";
8 | import { Form as FinalForm, Field } from "react-final-form";
9 | import { Form, Input, Button, message, Alert, Modal } from "antd";
10 | import { commentsAPI } from "../../api/api";
11 | import isEmpty from "lodash.isempty";
12 | import CommentsDesktop from "./CommentsDesktop";
13 | import CommentsMobile from "./CommentsMobile";
14 |
15 | export default function Comments({
16 | data,
17 | setDeleteReloadingFlag,
18 | setEditReloadingFlag,
19 | }) {
20 | const userState = useSelector((st) => st.user);
21 | const [deleteModal, setDeleteModal] = useState(false);
22 | const [selectedDeleteCommentID, setDeleteSelectedCommentID] = useState(null);
23 | const [selectedEditCommentID, setEditSelectedCommentID] = useState(null);
24 | const [deleteReloading, setDeleteReloading] = useState(false);
25 | const [editReloading, setEditReloading] = useState(false);
26 | const [initialValues, setInitialValues] = useState({});
27 | const [submissionErrors, setSubmissionErrors] = useState(null);
28 | const [width, setWidth] = useState(window.innerWidth);
29 |
30 | useEffect(() => {
31 | function handleResize() {
32 | setWidth(window.innerWidth);
33 | }
34 | window.addEventListener("resize", handleResize);
35 | return () => window.removeEventListener("resize", handleResize);
36 | }, [width]);
37 |
38 | const confirmDelete = async () => {
39 | try {
40 | await commentsAPI.delete(selectedDeleteCommentID);
41 | setDeleteModal(false);
42 | message.success("Comment deleted successfully");
43 | setDeleteReloadingFlag(!deleteReloading);
44 | setDeleteReloading(!deleteReloading);
45 | } catch (error) {
46 | console.log("Error deleting comment...", error.response ?? error);
47 | message.error("Error deleting comment");
48 | if (error.response && error.response.data) {
49 | message.error(error.response.data);
50 | } else message.error("Error deleting comment");
51 | setDeleteModal(false);
52 | }
53 | };
54 |
55 | const onSubmit = async (event) => {
56 | if (isEmpty(event) || !event.content) {
57 | setSubmissionErrors("Can't submit an empty comment");
58 | } else {
59 | setSubmissionErrors(null);
60 | }
61 |
62 | try {
63 | await commentsAPI.update({
64 | comment: { ...event },
65 | });
66 | message.success("Comment updated successfully");
67 | setEditReloadingFlag(!editReloading);
68 | setEditReloading(!editReloading);
69 | setEditSelectedCommentID(null);
70 | } catch (error) {
71 | console.log("Error editing comment...", error.response ?? error);
72 | message.error("Error editing comment");
73 | }
74 | };
75 |
76 | return (
77 |
78 | {data && Boolean(data.length)
79 | ? data.length === 1
80 | ? `${data.length} Comment`
81 | : `${data.length} Comments`
82 | : null}
83 |
84 | {data.map((comment, index) =>
85 | width >= 580 ? (
86 |
97 | ) : (
98 |
109 | )
110 | )}
111 |
112 |
confirmDelete()}
116 | onCancel={() => setDeleteModal(false)}
117 | centered
118 | >
119 | Are you sure you want to delete this comment?
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/client/src/pages/Posts/EditPost.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useHistory, useLocation } from "react-router-dom";
3 | import { Form, Input, Button, Tag, message } from "antd";
4 | import { Form as FinalForm, Field } from "react-final-form";
5 | import isEmpty from "lodash.isempty";
6 | import { useSelector } from "react-redux";
7 | import TextArea from "antd/lib/input/TextArea";
8 | import { postsAPI } from "./../../api/api";
9 |
10 | export default function EditPost() {
11 | const router = useHistory();
12 | const [initialValues, setInitialValues] = useState({});
13 | const [submissionErrors, setSubmissionErrors] = useState({});
14 | const userState = useSelector((st) => st.user);
15 | const location = useLocation();
16 | const [postID, setPostID] = useState(null);
17 |
18 | useEffect(() => {
19 | let id = null;
20 | (async () => {
21 | if (location.state && location.state.hasOwnProperty("postID")) {
22 | id = location.state.postID;
23 | setPostID(id);
24 | try {
25 | const { data: res } = await postsAPI.getOne(id);
26 | console.log(res);
27 | setInitialValues(res);
28 | } catch (error) {
29 | console.log("Error retrieving one post...", error);
30 | }
31 | } else {
32 | message.error("Post ID was not provided");
33 | router.goBack();
34 | }
35 | })();
36 | }, [location.state]);
37 |
38 | const onSubmit = async (event) => {
39 | try {
40 | await postsAPI.update({
41 | post: event,
42 | });
43 | message.success("Post updated successfully");
44 | router.goBack();
45 | } catch (error) {
46 | console.log("Error updating post...", error.response ?? error);
47 | if (error.response && error.response.data) {
48 | setSubmissionErrors(error.response.data);
49 | } else setSubmissionErrors({ err: "Update error" });
50 | }
51 | };
52 |
53 | const checkValidation = (values) => {
54 | const errors = {};
55 | if (!values.title?.trim()) {
56 | errors.title = "Please enter the post's title";
57 | }
58 | if (!values.content?.trim()) {
59 | errors.content = "Please enter the post's content";
60 | }
61 | return errors;
62 | };
63 |
64 | return (
65 |
66 |
Edit post
67 |
(
72 |
78 |
79 | {({ input, meta }) => (
80 |
81 |
82 | {meta.touched && meta.error && (
83 | {meta.error}
84 | )}
85 |
86 | )}
87 |
88 |
89 |
90 |
91 |
92 | {({ input, meta }) => (
93 |
94 |
95 | {meta.touched && meta.error && (
96 | {meta.error}
97 | )}
98 |
99 | )}
100 |
101 |
102 |
103 |
104 |
105 | {({ input, meta }) => (
106 |
107 |
108 | {meta.touched && meta.error && (
109 | {meta.error}
110 | )}
111 |
112 | )}
113 |
114 |
115 |
116 | {!isEmpty(submissionErrors) && (
117 |
118 | {Object.entries(submissionErrors).map(([key, value]) => (
119 |
120 | {value}
121 |
122 | ))}
123 |
124 | )}
125 |
126 |
127 |
130 |
133 |
134 |
135 | )}
136 | />
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/assets/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "./variables.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | code {
13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
14 | monospace;
15 | }
16 |
17 | //ANTD OVERRIDE
18 |
19 | .ant-card {
20 | margin: auto !important;
21 | }
22 | .ant-card-bordered {
23 | border-radius: 10px !important;
24 | width: 90%;
25 | margin: 1rem !important;
26 | // height: 500px;
27 | img {
28 | border-top-left-radius: 10px !important;
29 | border-top-right-radius: 10px !important;
30 | }
31 | }
32 |
33 | .ant-card-body {
34 | height: 150px;
35 | }
36 |
37 | .ant-card-actions {
38 | border-radius: 0 0 10px 10px;
39 | }
40 |
41 | .ant-divider-horizontal.ant-divider-with-text {
42 | margin: 2.5rem 0 0 0 !important;
43 | font-size: 1.4rem;
44 | }
45 |
46 | .ant-input,
47 | .ant-input-affix-wrapper {
48 | line-height: 2 !important;
49 | border-radius: 10px !important;
50 | }
51 |
52 | .no-radius {
53 | .ant-input,
54 | .ant-input-affix-wrapper {
55 | line-height: 2 !important;
56 | border-radius: 2px !important;
57 | }
58 | }
59 |
60 | .ant-btn {
61 | border-radius: 10px !important;
62 | height: 40px !important;
63 | }
64 |
65 | .ant-tag {
66 | margin: 0.5rem 0 !important;
67 | font-size: 0.9rem !important;
68 | border-radius: 5px !important;
69 | }
70 |
71 | //CUSTOM CLASSES
72 |
73 | .page-container {
74 | position: relative;
75 | min-height: 100vh;
76 | }
77 |
78 | .page-content {
79 | padding-bottom: 2.5rem;
80 | }
81 | .footer {
82 | position: absolute;
83 | bottom: 0;
84 | width: 100%;
85 | padding: 1rem;
86 | text-align: center;
87 | background-color: $DarkBlue;
88 | color: white;
89 | }
90 |
91 | .m-auto {
92 | margin: auto;
93 | }
94 |
95 | .loader-container {
96 | margin: 20px 0;
97 | margin-bottom: 20px;
98 | padding: 30px 50px;
99 | text-align: center;
100 | border-radius: 4px;
101 | }
102 |
103 | .disabled {
104 | background: white;
105 | color: black;
106 | border: none !important;
107 | box-shadow: none !important;
108 | font-weight: bold;
109 | padding-left: 0 !important;
110 | cursor: auto;
111 | &:hover,
112 | &:active {
113 | border: none !important;
114 | box-shadow: none !important;
115 | }
116 | }
117 |
118 | .form-container {
119 | width: 60%;
120 | background-color: white;
121 | color: black;
122 | margin: 2rem auto;
123 | padding: 1rem;
124 | -webkit-box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.77);
125 | box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.2);
126 | border: 1px solid $LightestGray;
127 | border-radius: 10px;
128 | }
129 |
130 | .float-right {
131 | float: right;
132 | }
133 |
134 | .unhoverable-menu-item {
135 | color: white !important;
136 | opacity: 1 !important;
137 | &:hover {
138 | background-color: transparent !important;
139 | color: inherit !important;
140 | }
141 | }
142 |
143 | .form {
144 | width: 100%;
145 | padding: 1rem;
146 | display: flex;
147 | flex-direction: column;
148 | }
149 |
150 | .buttons-wrapper-vertical {
151 | display: flex;
152 | flex-direction: column;
153 | gap: 0.5rem;
154 | }
155 |
156 | .buttons-wrapper-horizontal {
157 | display: flex;
158 | flex-direction: row;
159 | gap: 0.5rem;
160 | }
161 |
162 | .my-2-auto {
163 | margin: 2rem auto;
164 | }
165 |
166 | .centered-text {
167 | text-align: center;
168 | }
169 |
170 | .two-cols {
171 | display: flex;
172 | flex-direction: row;
173 | justify-content: space-between;
174 | align-items: baseline;
175 | margin: 2rem auto !important;
176 | }
177 |
178 | .center-panel {
179 | background-color: white;
180 | color: black;
181 | margin: 2rem;
182 | padding: 1rem;
183 | -webkit-box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.77);
184 | box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.2);
185 | border: 1px solid $LightestGray;
186 | border-radius: 10px;
187 | }
188 |
189 | .center-panel-small {
190 | background-color: white;
191 | color: black;
192 | margin: 2rem auto;
193 | padding: 1rem;
194 | -webkit-box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.77);
195 | box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.2);
196 | border: 1px solid $LightestGray;
197 | border-radius: 10px;
198 | width: 60%;
199 | }
200 |
201 | .left-panel {
202 | width: 40vw;
203 | background-color: white;
204 | color: black;
205 | margin: 2rem;
206 | padding: 1rem;
207 | -webkit-box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.77);
208 | box-shadow: -2px 5px 14px -1px rgba(0, 0, 0, 0.2);
209 | border: 1px solid $LightestGray;
210 | border-radius: 10px;
211 | }
212 | .right-panel {
213 | margin: 2rem;
214 | padding: 2rem;
215 | width: 37%;
216 | float: right;
217 | }
218 |
219 | .full-width {
220 | width: 100% !important;
221 | padding: 0.5rem 0.3rem !important;
222 | border-radius: 5px !important;
223 | }
224 |
--------------------------------------------------------------------------------
/client/src/pages/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import Jumbotron from "../../components/Jumbotron/Jumbotron";
3 | import "./Home.scss";
4 | import { Player } from "@lottiefiles/react-lottie-player";
5 | import { Button, Row, Col, Divider, Card, Spin, Alert } from "antd";
6 | import { RightCircleOutlined } from "@ant-design/icons";
7 | import { useHistory } from "react-router-dom";
8 | import { postsAPI } from "../../api/api";
9 | import defaultPostImage from "./../../assets/images/default-post-image.jpg";
10 | import { useSelector } from "react-redux";
11 | import PostsGrid from "../../components/PostsGrid/PostsGrid";
12 | const { Meta } = Card;
13 |
14 | export default function Home() {
15 | const router = useHistory();
16 | const userState = useSelector((st) => st.user);
17 | const [postsData, setPostsData] = useState([]);
18 | const [width, setWidth] = useState(window.innerWidth);
19 | const [errorMsg, setErrorMsg] = useState(null);
20 | const [reload, setReload] = useState(false);
21 |
22 | useEffect(() => {
23 | function handleResize() {
24 | setWidth(window.innerWidth);
25 | }
26 | window.addEventListener("resize", handleResize);
27 | return () => window.removeEventListener("resize", handleResize);
28 | }, [width]);
29 |
30 | useEffect(() => {
31 | (async () => {
32 | try {
33 | const { data: res } = await postsAPI.getAll();
34 | setPostsData(res);
35 | setErrorMsg(null);
36 | } catch (error) {
37 | setErrorMsg("Error loading posts data");
38 | console.log("Error retrieving all posts...", error);
39 | }
40 | })();
41 | }, [reload]);
42 |
43 | return (
44 |
45 | {userState.isLoggedIn ? (
46 | <>
47 |
48 |
49 | {width <= 650 ? (
50 |
51 |
A blogging website for
52 | EVERYONE
53 | }
57 | size="large"
58 | onClick={() => router.push("/posts/new")}
59 | >
60 | Write a post now!
61 |
62 |
63 | ) : (
64 | <>
65 |
66 |
A blogging website for
67 | EVERYONE
68 | }
72 | size="large"
73 | onClick={() => router.push("/posts/new")}
74 | >
75 | Get Started
76 |
77 |
78 |
85 | >
86 | )}
87 |
88 |
89 | >
90 | ) : (
91 | <>
92 |
93 |
94 | {width <= 650 ? (
95 |
96 |
A blogging website for
97 | EVERYONE
98 | }
102 | size="large"
103 | onClick={() => router.push("/login")}
104 | >
105 | Get Started
106 |
107 |
108 | ) : (
109 | <>
110 |
111 |
A blogging website for
112 | EVERYONE
113 | }
117 | size="large"
118 | onClick={() => router.push("/login")}
119 | >
120 | Get Started
121 |
122 |
123 |
130 | >
131 | )}
132 |
133 |
134 | >
135 | )}
136 |
Most recent posts
137 | {errorMsg ? (
138 |
141 | ) : postsData && Boolean(postsData.length) ? (
142 |
setReload(param)} />
143 | ) : (
144 |
145 |
146 |
147 | )}
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/pages/Posts/Post.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useHistory, useLocation } from "react-router-dom";
3 | import { Button, message, Image, Spin, Alert, Modal, Divider } from "antd";
4 | import { EditFilled, DeleteFilled } from "@ant-design/icons";
5 | import { useSelector } from "react-redux";
6 | import { postsAPI } from "./../../api/api";
7 | import "./Post.scss";
8 | import defaultPostImage from "./../../assets/images/default-post-image.jpg";
9 | import defaultUser from "./../../assets/images/default-user.png";
10 | import moment from "moment";
11 | import Avatar from "antd/lib/avatar/avatar";
12 | import CommentForm from "../Comments/CommentForm";
13 | import Comments from "../Comments/Comments";
14 |
15 | export default function Post() {
16 | const [postData, setPostData] = useState({});
17 | const router = useHistory();
18 | const userState = useSelector((st) => st.user);
19 | const location = useLocation();
20 | const [deleteModal, setDeleteModal] = useState(false);
21 | const [deletePostID, setDeletePostID] = useState(null);
22 | const [errorMsg, setErrorMsg] = useState(null);
23 | const [reloadDelete, setReloadDelete] = useState(null);
24 | const [reloadEdit, setReloadEdit] = useState(null);
25 | const [reloadPost, setReloadPost] = useState(null);
26 |
27 | useEffect(() => {
28 | let id = null;
29 | (async () => {
30 | if (location.state && location.state.hasOwnProperty("postID")) {
31 | id = location.state.postID;
32 | try {
33 | const { data: res } = await postsAPI.getOne(id);
34 | setPostData(res);
35 | setErrorMsg(null);
36 | } catch (error) {
37 | setPostData({});
38 | setErrorMsg("Error loading post data");
39 | console.log("Error retrieving one post...", error);
40 | }
41 | } else {
42 | message.error("An error occured while retrieving post ID");
43 | router.push("/");
44 | }
45 | })();
46 | }, [location.state, reloadDelete, reloadPost, reloadEdit]);
47 |
48 | const confirmDelete = async () => {
49 | try {
50 | await postsAPI.delete(deletePostID);
51 | setDeleteModal(false);
52 | message.success("Post deleted successfully");
53 | router.goBack();
54 | } catch (error) {
55 | console.log("Error deleting post...", error.response ?? error);
56 | message.error("Error deleting post");
57 | if (error.response && error.response.data) {
58 | message.error(error.response.data);
59 | } else message.error("Error deleting post");
60 | setDeleteModal(false);
61 | }
62 | };
63 |
64 | return (
65 |
66 | {errorMsg ? (
67 |
70 | ) : Object.keys(postData).length === 0 ? (
71 |
72 |
73 |
74 | ) : (
75 | <>
76 |
77 |
{postData.title}
78 |
79 |
80 |
84 |
85 | {postData.createdByName}
86 |
87 |
{moment(postData.createdAt).format("DD MMMM YYYY")}
88 |
89 |
90 |
91 |
94 |
95 |
{postData.content}
96 | {postData.createdBy === userState.user.id && (
97 |
98 | }
102 | onClick={() =>
103 | router.push("/posts/edit", { postID: postData._id })
104 | }
105 | >
106 | Edit
107 |
108 |
109 | }
113 | danger
114 | onClick={() => {
115 | setDeletePostID(postData._id);
116 | setDeleteModal(true);
117 | }}
118 | >
119 | Delete
120 |
121 |
122 | )}
123 | {Boolean(userState.user.id) && (
124 | <>
125 |
126 |
setReloadPost(value)}
130 | />
131 | >
132 | )}
133 | setReloadDelete(value)}
136 | setEditReloadingFlag={(value) => setReloadEdit(value)}
137 | />
138 |
139 | confirmDelete()}
143 | onCancel={() => setDeleteModal(false)}
144 | centered
145 | >
146 | Are you sure you want to delete post?
147 |
148 | >
149 | )}
150 |
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/client/src/pages/Signup/SignupForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import { Form, Input, Button, Tag, message } from "antd";
4 | import { Form as FinalForm, Field } from "react-final-form";
5 | import { authAPI } from "./../../api/api";
6 | import isEmpty from "lodash.isempty";
7 |
8 | export default function SignupForm() {
9 | const router = useHistory();
10 | const [initialValues, setInitialValues] = useState({});
11 | const [submissionErrors, setSubmissionErrors] = useState({});
12 |
13 | const onSubmit = async (event) => {
14 | try {
15 | await authAPI.signup({ user: event });
16 | message.success("User created successfully");
17 | router.push("/login");
18 | } catch (error) {
19 | console.log("Error registering a new user...", error.response ?? error);
20 | if (error.response && error.response.data) {
21 | setSubmissionErrors(error.response.data);
22 | } else setSubmissionErrors({ err: "Signup error" });
23 | }
24 | };
25 |
26 | const checkValidation = (values) => {
27 | const errors = {};
28 | if (!values.userName?.trim()) {
29 | errors.userName = "Please enter the userName";
30 | }
31 | if (!values.password?.trim()) {
32 | errors.password = "Please enter the password";
33 | }
34 | if (!values.confirmPassword?.trim()) {
35 | errors.confirmPassword = "Please enter the password confirmation";
36 | } else if (values.confirmPassword !== values.password) {
37 | errors.confirmPassword = "Passwords do not match";
38 | }
39 | if (!values.email?.trim()) {
40 | errors.email = "Please enter the email";
41 | }
42 | return errors;
43 | };
44 |
45 | return (
46 | (
51 |
57 |
58 | {({ input, meta }) => (
59 |
60 |
61 | {meta.touched && meta.error && (
62 | {meta.error}
63 | )}
64 |
65 | )}
66 |
67 |
68 |
69 |
70 | {({ input, meta }) => (
71 |
72 |
73 | {meta.touched && meta.error && (
74 | {meta.error}
75 | )}
76 |
77 | )}
78 |
79 |
80 |
81 |
82 | {({ input, meta }) => (
83 |
84 |
85 | {meta.touched && meta.error && (
86 | {meta.error}
87 | )}
88 |
89 | )}
90 |
91 |
92 |
93 |
94 | {({ input, meta }) => (
95 |
96 |
97 | {meta.touched && meta.error && (
98 | {meta.error}
99 | )}
100 |
101 | )}
102 |
103 |
104 |
105 |
106 | {({ input, meta }) => (
107 |
108 |
109 | {meta.touched && meta.error && (
110 | {meta.error}
111 | )}
112 |
113 | )}
114 |
115 |
116 |
117 |
118 | {({ input, meta }) => (
119 |
120 |
121 | {meta.touched && meta.error && (
122 | {meta.error}
123 | )}
124 |
125 | )}
126 |
127 |
128 |
129 | {!isEmpty(submissionErrors) && (
130 |
131 | {Object.entries(submissionErrors).map(([key, value]) => (
132 |
133 | {value}
134 |
135 | ))}
136 |
137 | )}
138 |
139 |
140 |
143 |
150 |
151 |
152 | )}
153 | />
154 | );
155 | }
156 |
--------------------------------------------------------------------------------
/client/src/pages/Users/UserProfile.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import { Form, Input, Button, Tag, message, Image, Spin, Alert } from "antd";
4 | import {
5 | EditFilled,
6 | LockFilled,
7 | SaveFilled,
8 | CaretLeftOutlined,
9 | } from "@ant-design/icons";
10 | import { Form as FinalForm, Field } from "react-final-form";
11 | import isEmpty from "lodash.isempty";
12 | import { useDispatch, useSelector } from "react-redux";
13 | import { usersAPI } from "./../../api/api";
14 | import defaultUser from "./../../assets/images/default-user.png";
15 | import "./UserProfile.scss";
16 | import { userAuthActions } from "./../../redux/actions/actionCreator";
17 |
18 | export default function UserProfile() {
19 | const router = useHistory();
20 | const [initialValues, setInitialValues] = useState({});
21 | const [submissionErrors, setSubmissionErrors] = useState({});
22 | const userState = useSelector((st) => st.user);
23 | const [editing, setEditing] = useState(false);
24 | const [editingPassword, setEditingPassword] = useState(false);
25 | const dispatch = useDispatch();
26 | const [errorMsg, setErrorMsg] = useState(null);
27 |
28 | useEffect(() => {
29 | (async () => {
30 | if (userState && userState.user && userState.user.id) {
31 | try {
32 | const { data: res } = await usersAPI.getOne(userState.user.id);
33 | setInitialValues(res);
34 | setErrorMsg(null);
35 | } catch (error) {
36 | setInitialValues({});
37 | setErrorMsg("Error loading user profile");
38 | console.log("Error retrieving user data...", error);
39 | }
40 | } else {
41 | message.error("An error occured while retrieving user ID");
42 | router.push("/");
43 | }
44 | })();
45 | }, []);
46 |
47 | const checkValidation = (values) => {
48 | const errors = {};
49 | if (editing && !values.userName?.trim()) {
50 | errors.userName = "Please enter the userName";
51 | }
52 | if (editingPassword && !values.oldPassword?.trim()) {
53 | errors.password = "Please enter the old password";
54 | }
55 | if (editingPassword && !values.password?.trim()) {
56 | errors.password = "Please enter the password";
57 | }
58 | if (editingPassword && !values.confirmPassword?.trim()) {
59 | errors.confirmPassword = "Please enter the password confirmation";
60 | } else if (editingPassword && values.confirmPassword !== values.password) {
61 | errors.confirmPassword = "Passwords do not match";
62 | }
63 | if (editing && !values.email?.trim()) {
64 | errors.email = "Please enter the email";
65 | }
66 | return errors;
67 | };
68 |
69 | const onSubmit = async (event) => {
70 | setSubmissionErrors([]);
71 | try {
72 | await usersAPI.update({ user: event });
73 | dispatch(userAuthActions.updateUser(event));
74 | message.success("User profile updated successfully");
75 | setEditing(false);
76 | setEditingPassword(false);
77 | setInitialValues(event);
78 | } catch (error) {
79 | console.log("Error updating user profile..", error.response ?? error);
80 | if (error.response && error.response.data) {
81 | setSubmissionErrors(error.response.data);
82 | } else setSubmissionErrors({ err: "Profile update error" });
83 | }
84 | };
85 |
86 | return (
87 |
88 |
89 | {errorMsg ? (
90 |
93 | ) : Object.keys(initialValues).length === 0 ? (
94 |
95 |
96 |
97 | ) : (
98 | <>
99 |
100 | {initialValues.userName}'s Profile
101 |
102 |
103 |
107 |
108 |
311 | >
312 | )}
313 |
314 |
315 | );
316 | }
317 |
--------------------------------------------------------------------------------