Skip to content

Commit 2d25b6a

Browse files
authored
#266 A user should be able to comment and react to a blog post (#276)
* Fix(#266): resolved conflicts * Fix(#266): added comments and reactions to a blog
1 parent d048945 commit 2d25b6a

File tree

11 files changed

+1022
-77
lines changed

11 files changed

+1022
-77
lines changed

src/pages/Blogs/BlogComment.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { useState, useEffect } from "react";
2+
import { useDispatch, useSelector } from "react-redux";
3+
import { RootState } from "../../redux/reducers";
4+
import {
5+
getCommentsByBlogId,
6+
createCommentAction,
7+
fetchRepliesByComment,
8+
addReplyToComment,
9+
} from "../../redux/actions/commentActions";
10+
const profile: string = require("../../assets/avatar.png").default;
11+
12+
interface BlogCommentProps {
13+
blogId: string;
14+
}
15+
16+
const BlogComment: React.FC<BlogCommentProps> = ({ blogId }) => {
17+
const dispatch = useDispatch();
18+
19+
const {
20+
comment_data: comments,
21+
isCommentLoading,
22+
errors,
23+
repliesByCommentId,
24+
} = useSelector((state: RootState) => state.comments);
25+
26+
const [newComment, setNewComment] = useState("");
27+
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
28+
const [replyContent, setReplyContent] = useState("");
29+
30+
useEffect(() => {
31+
if (blogId) {
32+
dispatch(getCommentsByBlogId(blogId));
33+
}
34+
}, [dispatch, blogId]);
35+
36+
useEffect(() => {
37+
if (activeCommentId) {
38+
dispatch(fetchRepliesByComment(activeCommentId));
39+
}
40+
}, [dispatch, activeCommentId]);
41+
42+
const handleAddComment = () => {
43+
if (newComment.trim()) {
44+
dispatch(createCommentAction(blogId, newComment));
45+
setNewComment("");
46+
}
47+
};
48+
49+
const handleAddReply = (commentId: string) => {
50+
const userId = localStorage.getItem("userId");
51+
if (!userId) {
52+
alert("You must be logged in to reply.");
53+
return;
54+
}
55+
56+
if (replyContent.trim()) {
57+
dispatch(addReplyToComment(replyContent, commentId));
58+
setReplyContent("");
59+
}
60+
};
61+
62+
return (
63+
<div className="p-6 bg-gray-900 text-white rounded-lg space-y-6">
64+
<h2 className="text-2xl font-bold">{`${comments.length} Comments`}</h2>
65+
66+
<div className="flex gap-4 items-center">
67+
<input
68+
type="text"
69+
value={newComment}
70+
onChange={(e) => setNewComment(e.target.value)}
71+
placeholder="Add your comment here..."
72+
className="flex-grow p-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-green-400"
73+
/>
74+
<button
75+
onClick={handleAddComment}
76+
disabled={isCommentLoading}
77+
className="rounded py-1 px-4 bg-green text-white transition-colors dark:hover:bg-dark-frame-bg hover:text-green hover:border hover:border-green"
78+
>
79+
Add Comment
80+
</button>
81+
</div>
82+
83+
{comments.length > 0 ? (
84+
<div className="space-y-4">
85+
{comments.map((comment) => (
86+
<div
87+
key={comment.id}
88+
className="bg-gray-800 rounded-lg p-4 space-y-4"
89+
>
90+
<div className="flex items-center gap-4">
91+
<img
92+
src={profile}
93+
alt="Profile"
94+
className="w-10 h-10 rounded-full bg-gray-700"
95+
/>
96+
<div>
97+
<h3 className="font-semibold">{`${comment.user.firstname} ${comment.user.lastname}`}</h3>
98+
<p className="text-sm text-gray-400">
99+
{new Date(comment.createdAt).toLocaleDateString("en-GB", {
100+
weekday: 'long',
101+
year: 'numeric',
102+
month: '2-digit',
103+
day: '2-digit',
104+
}).replace(/\//g, "-")}
105+
</p>
106+
</div>
107+
</div>
108+
109+
<p className="text-gray-300">{comment.content}</p>
110+
111+
</div>
112+
))}
113+
</div>
114+
) : (
115+
<p className="text-gray-400">No comments yet.</p>
116+
)}
117+
118+
{errors && <p className="text-red-500">{errors}</p>}
119+
</div>
120+
);
121+
};
122+
123+
export default BlogComment;

src/pages/Blogs/BlogReactions.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React, { useState, useEffect } from "react";
2+
import { useDispatch, useSelector } from "react-redux";
3+
import { RootState } from "../../redux/reducers";
4+
import {
5+
getReactionsByBlogId,
6+
addReactionAction,
7+
removeReactionAction,
8+
} from "../../redux/actions/reactionActions";
9+
import {getCommentsByBlogId} from "../../redux/actions/commentActions"
10+
11+
interface Reaction {
12+
[type: string]: number;
13+
}
14+
15+
interface BlogReactionProps {
16+
blogId: string;
17+
}
18+
19+
const reactionTypes = [
20+
{ type: "LIKE", label: "Like", emoji: "👍" },
21+
{ type: "CELEBRATE", label: "Celebrate", emoji: "🎉" },
22+
{ type: "LOVE", label: "Love", emoji: "❤️" },
23+
{ type: "SUPPORT", label: "Support", emoji: "👏" },
24+
{ type: "FUNNY", label: "Funny", emoji: "😂" },
25+
];
26+
27+
const BlogReaction: React.FC<BlogReactionProps> = ({ blogId }) => {
28+
const dispatch = useDispatch();
29+
const [currentReaction, setCurrentReaction] = useState<string | null>(null);
30+
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
31+
32+
const { reactions, isReactionLoading } = useSelector(
33+
(state: RootState) => state.reactions
34+
);
35+
36+
const typedReactions: Reaction = reactions;
37+
38+
useEffect(() => {
39+
const storedReaction = localStorage.getItem(`reaction_${blogId}`);
40+
if (storedReaction) {
41+
setCurrentReaction(storedReaction);
42+
}
43+
44+
if (blogId) {
45+
dispatch(getReactionsByBlogId(blogId));
46+
}
47+
}, [dispatch, blogId]);
48+
49+
const handleAddReaction = async (type: string) => {
50+
if (type === currentReaction) {
51+
await dispatch(removeReactionAction(blogId));
52+
setCurrentReaction(null);
53+
localStorage.removeItem(`reaction_${blogId}`);
54+
} else {
55+
if (currentReaction) {
56+
await dispatch(removeReactionAction(blogId));
57+
localStorage.removeItem(`reaction_${blogId}`);
58+
}
59+
60+
await dispatch(addReactionAction(blogId, type));
61+
setCurrentReaction(type);
62+
localStorage.setItem(`reaction_${blogId}`, type);
63+
}
64+
65+
dispatch(getReactionsByBlogId(blogId));
66+
dispatch(getCommentsByBlogId(blogId));
67+
setShowReactionsMenu(false);
68+
};
69+
70+
const totalReactions = typedReactions
71+
? Object.values(typedReactions).reduce((total, count) => total + count, 0)
72+
: 0;
73+
74+
return (
75+
<div className="relative">
76+
<button
77+
className="rounded-full text-white px-4 py-2 transition"
78+
onMouseEnter={() => setShowReactionsMenu(true)}
79+
onMouseLeave={() => setShowReactionsMenu(false)}
80+
>
81+
<span role="img" aria-label="like" className="text-xl flex items-center gap-2">
82+
👍 <span>Like</span>
83+
</span>
84+
</button>
85+
86+
{showReactionsMenu && (
87+
<div
88+
className="absolute top-[-50px] left-0 flex gap-1 bg-white shadow-lg rounded-xl p-2 z-10"
89+
onMouseEnter={() => setShowReactionsMenu(true)}
90+
onMouseLeave={() => setShowReactionsMenu(false)}
91+
>
92+
{reactionTypes.map(({ type, label, emoji }) => (
93+
<button
94+
key={type}
95+
className={`flex flex-col items-center hover:bg-gray-300 p-2 rounded-xl transition ${
96+
type === currentReaction ? "bg-blue-200" : ""
97+
}`}
98+
onClick={() => handleAddReaction(type)}
99+
>
100+
<span className="text-2xl">{emoji}</span>
101+
<span className="text-sm text-black">{label}</span>
102+
</button>
103+
))}
104+
</div>
105+
)}
106+
107+
<div className="text-sm text-gray-400 mb-4">
108+
{`${totalReactions} reactions`}
109+
</div>
110+
111+
<div className="mb-4 flex items-center gap-2">
112+
{reactionTypes.map(({ type, emoji }) => {
113+
const count = typedReactions[type] || 0;
114+
115+
if (count > 0){
116+
return (
117+
<div key={type} className="flex items-center gap-1 text-gray-600">
118+
<span className="text-xl">{emoji}</span>
119+
<span>{count}</span>
120+
</div>
121+
);
122+
}
123+
return null;
124+
})}
125+
</div>
126+
</div>
127+
);
128+
};
129+
130+
export default BlogReaction;

src/pages/Blogs/singleBlog.tsx

Lines changed: 4 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import SingleBlogSkeleton from '../../skeletons/singleBlogSkeleton';
99
import * as icons from "react-icons/ai";
1010
import { useSelector } from 'react-redux';
1111
import { toast } from 'react-toastify';
12+
import BlogComment from './BlogComment';
13+
import BlogReaction from './BlogReactions';
1214

1315
import { useNavigate } from 'react-router-dom';
1416

@@ -181,6 +183,8 @@ const SingleBlogView = () => {
181183
)}
182184
</div>
183185
</div>
186+
{id && <BlogReaction blogId={id} />}
187+
{id && <BlogComment blogId={id} />}
184188
{topArticles && topArticles.length > 0 ? (
185189
<div className='mt-10'>
186190
<h1>Related Articles</h1>
@@ -205,81 +209,6 @@ const SingleBlogView = () => {
205209
): (
206210
<p>No related articles available</p>
207211
)}
208-
<div className="flex items-center gap-4">
209-
<button onClick={handleLike} className="flex items-center gap-2">
210-
<Heart
211-
className={`w-6 h-6 ${
212-
isLiked ? "fill-green-400 text-green-400" : "dark:text-white"
213-
}`}
214-
/>
215-
<span>{blog.likes.length}</span>
216-
</button>
217-
</div>
218-
<div className="flex flex-row my-2 gap-4 w-full items-center justify-start">
219-
<input
220-
type="text"
221-
value={comment}
222-
onChange={(e) => setComment(e.target.value)}
223-
placeholder="Add your comment here..."
224-
className="px-4 py-2 w-1/2 dark:bg-slate-800 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-400"
225-
/>
226-
<button
227-
onClick={handleComment}
228-
className="rounded py-1 px-4 bg-green text-white transition-colors dark:hover:bg-dark-frame-bg hover:text-green hover:border hover:border-green"
229-
>
230-
Comment
231-
</button>
232-
</div>
233-
<div className="space-y-4">
234-
<h2 className="text-xl font-semibold">
235-
{blog.comments.length} Comments
236-
</h2>
237-
{blog.comments.length > 0 ? (
238-
<>
239-
{blog.comments.map((comment) => (
240-
<div
241-
key={comment.id}
242-
className="dark:bg-slate-800 bg-slate-400 rounded-lg p-4 space-y-2"
243-
>
244-
<div className="flex items-center gap-3">
245-
<div className="w-10 h-10 bg-slate-300 dark:bg-slate-700 rounded-full overflow-hidden">
246-
<img
247-
src="/api/placeholder/40/40"
248-
alt={comment.user.firstname}
249-
className="w-full h-full object-cover"
250-
/>
251-
</div>
252-
<div>
253-
<h3 className="font-medium">
254-
{comment.user.firstname}
255-
</h3>
256-
<p className="text-sm text-slate-400">
257-
{comment.created_at}
258-
</p>
259-
</div>
260-
</div>
261-
262-
<p className="text-slate-300">{comment.content}</p>
263-
264-
<div className="flex items-center gap-4 text-sm dark:text-slate-400">
265-
<button className="flex items-center gap-1">
266-
<MessageCircle className="w-4 h-4" />
267-
{comment.replies.length} Replies
268-
</button>
269-
<button className="flex items-center gap-1">
270-
<Heart className="w-4 h-4" />
271-
{comment.likes.length}
272-
</button>
273-
</div>
274-
</div>
275-
))}
276-
</>
277-
) : (
278-
<div>
279-
<p className="text-left">No comments yet</p>
280-
</div>
281-
)}
282-
</div>
283212

284213
<div className={`${userId === blog.author.id ? "" : "hidden"} flex flex-col gap-2 mt-10`}>
285214
<h1 className='text-red-500 font-bold'>Danger Zone</h1>

0 commit comments

Comments
 (0)