-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix(#266): added comments and reactions
- Loading branch information
1 parent
8e50e92
commit 28f8885
Showing
11 changed files
with
1,004 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import React, { useState, useEffect } from "react"; | ||
import { useDispatch, useSelector } from "react-redux"; | ||
import { RootState } from "../../redux/reducers"; | ||
import { | ||
getCommentsByBlogId, | ||
createCommentAction, | ||
fetchRepliesByComment, | ||
addReplyToComment, | ||
} from "../../redux/actions/commentActions"; | ||
const profile: string = require("../../assets/avatar.png").default; | ||
|
||
interface BlogCommentProps { | ||
blogId: string; | ||
} | ||
|
||
const BlogComment: React.FC<BlogCommentProps> = ({ blogId }) => { | ||
const dispatch = useDispatch(); | ||
|
||
const { | ||
comment_data: comments, | ||
isCommentLoading, | ||
errors, | ||
repliesByCommentId, | ||
} = useSelector((state: RootState) => state.comments); | ||
|
||
const [newComment, setNewComment] = useState(""); | ||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null); | ||
const [replyContent, setReplyContent] = useState(""); | ||
|
||
useEffect(() => { | ||
if (blogId) { | ||
dispatch(getCommentsByBlogId(blogId)); | ||
} | ||
}, [dispatch, blogId]); | ||
|
||
useEffect(() => { | ||
if (activeCommentId) { | ||
dispatch(fetchRepliesByComment(activeCommentId)); | ||
} | ||
}, [dispatch, activeCommentId]); | ||
|
||
const handleAddComment = () => { | ||
if (newComment.trim()) { | ||
dispatch(createCommentAction(blogId, newComment)); | ||
setNewComment(""); | ||
} | ||
}; | ||
|
||
const handleAddReply = (commentId: string) => { | ||
const userId = localStorage.getItem("userId"); | ||
if (!userId) { | ||
alert("You must be logged in to reply."); | ||
return; | ||
} | ||
|
||
if (replyContent.trim()) { | ||
dispatch(addReplyToComment(replyContent, commentId)); | ||
setReplyContent(""); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="space-y-4"> | ||
<h2 className="text-xl font-semibold"> | ||
{`${comments.length} Comments`} | ||
</h2> | ||
|
||
<div className="flex flex-row my-2 gap-4 w-full items-center justify-start"> | ||
<textarea | ||
value={newComment} | ||
onChange={(e) => setNewComment(e.target.value)} | ||
placeholder="Write your comment here..." | ||
className="px-4 py-2 w-1/2 bg-gray-800 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-400" | ||
/> | ||
<button | ||
onClick={handleAddComment} | ||
disabled={isCommentLoading} | ||
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" | ||
> | ||
Add Comment | ||
</button> | ||
</div> | ||
|
||
{comments.length > 0 ? ( | ||
<div> | ||
{comments.map((comment) => ( | ||
<div | ||
key={comment.id} | ||
className="dark:bg-slate-800 bg-slate-400 rounded-lg p-4 space-y-2" | ||
> | ||
<div className="flex items-center gap-3 w-1/2"> | ||
<div className="w-10 h-10 bg-slate-300 dark:bg-slate-700 rounded-full overflow-hidden"> | ||
<img | ||
src={profile} | ||
className="w-full h-full object-cover" | ||
/> | ||
</div> | ||
<div> | ||
<h3 className="font-medium">{`${comment.user.firstname} ${comment.user.lastname}`}</h3> | ||
</div> | ||
</div> | ||
<p className="text-white">{comment.content}</p> | ||
|
||
<button | ||
onClick={() => | ||
setActiveCommentId(activeCommentId === comment.id ? null : comment.id) | ||
} | ||
className="text-green text-sm" | ||
> | ||
{activeCommentId === comment.id ? "Hide Replies" : "View Replies"} | ||
</button> | ||
{activeCommentId === comment.id && ( | ||
<div> | ||
{repliesByCommentId[comment.id]?.length > 0 ? ( | ||
repliesByCommentId[comment.id].map((reply) => ( | ||
<div key={reply.id} className="ml-4 mt-2"> | ||
<p className="text-gray-200">{reply.content}</p> | ||
<span className="text-xs text-gray-400"> | ||
- {reply.user.firstname || "Anonymous"} {reply.user.lastname || ""} | ||
</span> | ||
</div> | ||
)) | ||
) : ( | ||
<p>No replies yet</p> | ||
)} | ||
<div className="mt-2"> | ||
<textarea | ||
value={replyContent} | ||
onChange={(e) => setReplyContent(e.target.value)} | ||
placeholder="Write a reply..." | ||
className="bg-gray-800 px-2 py-1 w-1/2 border rounded-md mr-4" | ||
/> | ||
<button | ||
onClick={() => handleAddReply(comment.id)} | ||
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" | ||
> | ||
Reply | ||
</button> | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
) : ( | ||
<div> | ||
<p className="text-left">No comments yet</p> | ||
</div> | ||
)} | ||
|
||
{errors && <p className="text-red-500">{errors}</p>} | ||
</div> | ||
); | ||
}; | ||
|
||
export default BlogComment; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React, { useState, useEffect } from "react"; | ||
import { useDispatch, useSelector } from "react-redux"; | ||
import { RootState } from "../../redux/reducers"; | ||
import { | ||
getReactionsByBlogId, | ||
addReactionAction, | ||
removeReactionAction, | ||
} from "../../redux/actions/reactionActions"; | ||
|
||
interface Reaction { | ||
[type: string]: number; | ||
} | ||
|
||
interface BlogReactionProps { | ||
blogId: string; | ||
} | ||
|
||
const reactionTypes = [ | ||
{ type: "LIKE", label: "Like", emoji: "👍" }, | ||
{ type: "CELEBRATE", label: "Celebrate", emoji: "🎉" }, | ||
{ type: "LOVE", label: "Love", emoji: "❤️" }, | ||
{ type: "SUPPORT", label: "Support", emoji: "👏" }, | ||
{ type: "FUNNY", label: "Funny", emoji: "😂" }, | ||
]; | ||
|
||
const BlogReaction: React.FC<BlogReactionProps> = ({ blogId }) => { | ||
const dispatch = useDispatch(); | ||
const [currentReaction, setCurrentReaction] = useState<string | null>(null); | ||
const [showReactionsMenu, setShowReactionsMenu] = useState(false); | ||
|
||
const { reactions, isReactionLoading } = useSelector( | ||
(state: RootState) => state.reactions | ||
); | ||
|
||
const typedReactions: Reaction = reactions; | ||
|
||
useEffect(() => { | ||
if (blogId) { | ||
dispatch(getReactionsByBlogId(blogId)); | ||
} | ||
}, [dispatch, blogId]); | ||
|
||
const handleAddReaction = async (type: string) => { | ||
if (type === currentReaction) { | ||
await dispatch(removeReactionAction(blogId)); | ||
setCurrentReaction(null); | ||
} else { | ||
if (currentReaction) { | ||
await dispatch(removeReactionAction(blogId)); | ||
} | ||
await dispatch(addReactionAction(blogId, type)); | ||
setCurrentReaction(type); | ||
} | ||
dispatch(getReactionsByBlogId(blogId)); | ||
setShowReactionsMenu(false); | ||
}; | ||
|
||
const totalReactions = typedReactions | ||
? Object.values(typedReactions).reduce((total, count) => total + count, 0) | ||
: 0; | ||
|
||
useEffect(() => { | ||
}, [typedReactions, totalReactions]); | ||
|
||
return ( | ||
<div className="relative"> | ||
<button | ||
className="rounded-full text-white px-4 py-2 transition" | ||
onMouseEnter={() => setShowReactionsMenu(true)} | ||
onMouseLeave={() => setShowReactionsMenu(false)} | ||
> | ||
<span role="img" aria-label="like" className="text-xl flex items-center gap-2"> | ||
👍 <span>Like</span> | ||
</span> | ||
</button> | ||
|
||
{showReactionsMenu && ( | ||
<div | ||
className="absolute top-[-50px] left-0 flex gap-1 bg-white shadow-lg rounded-xl p-2 z-10" | ||
onMouseEnter={() => setShowReactionsMenu(true)} | ||
onMouseLeave={() => setShowReactionsMenu(false)} | ||
> | ||
{reactionTypes.map(({ type, label, emoji }) => ( | ||
<button | ||
key={type} | ||
className={`flex flex-col items-center hover:bg-gray-300 p-2 rounded-xl transition ${ | ||
type === currentReaction ? "bg-blue-200" : "" | ||
}`} | ||
onClick={() => handleAddReaction(type)} | ||
> | ||
<span className="text-2xl">{emoji}</span> | ||
<span className="text-sm text-black">{label}</span> | ||
</button> | ||
))} | ||
</div> | ||
)} | ||
|
||
<div className="text-sm text-gray-400 mb-4"> | ||
{`${totalReactions} reactions`} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BlogReaction; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.