Skip to content

Commit

Permalink
Fix(#266): added comments and reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
Philimuhire committed Nov 28, 2024
1 parent 8e50e92 commit 28f8885
Show file tree
Hide file tree
Showing 11 changed files with 1,004 additions and 76 deletions.
156 changes: 156 additions & 0 deletions src/pages/Blogs/BlogComment.tsx
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;
105 changes: 105 additions & 0 deletions src/pages/Blogs/BlogReactions.tsx
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;
79 changes: 4 additions & 75 deletions src/pages/Blogs/singleBlog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import SingleBlogSkeleton from '../../skeletons/singleBlogSkeleton';
import * as icons from "react-icons/ai";
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import BlogComment from './BlogComment';
import BlogReaction from './BlogReactions';

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

Expand Down Expand Up @@ -181,6 +183,8 @@ const SingleBlogView = () => {
)}
</div>
</div>
{id && <BlogReaction blogId={id} />}
{id && <BlogComment blogId={id} />}
{topArticles && topArticles.length > 0 ? (
<div className='mt-10'>
<h1>Related Articles</h1>
Expand All @@ -205,81 +209,6 @@ const SingleBlogView = () => {
): (
<p>No related articles available</p>
)}
<div className="flex items-center gap-4">
<button onClick={handleLike} className="flex items-center gap-2">
<Heart
className={`w-6 h-6 ${
isLiked ? "fill-green-400 text-green-400" : "dark:text-white"
}`}
/>
<span>{blog.likes.length}</span>
</button>
</div>
<div className="flex flex-row my-2 gap-4 w-full items-center justify-start">
<input
type="text"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Add your comment here..."
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"
/>
<button
onClick={handleComment}
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"
>
Comment
</button>
</div>
<div className="space-y-4">
<h2 className="text-xl font-semibold">
{blog.comments.length} Comments
</h2>
{blog.comments.length > 0 ? (
<>
{blog.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">
<div className="w-10 h-10 bg-slate-300 dark:bg-slate-700 rounded-full overflow-hidden">
<img
src="/api/placeholder/40/40"
alt={comment.user.firstname}
className="w-full h-full object-cover"
/>
</div>
<div>
<h3 className="font-medium">
{comment.user.firstname}
</h3>
<p className="text-sm text-slate-400">
{comment.created_at}
</p>
</div>
</div>

<p className="text-slate-300">{comment.content}</p>

<div className="flex items-center gap-4 text-sm dark:text-slate-400">
<button className="flex items-center gap-1">
<MessageCircle className="w-4 h-4" />
{comment.replies.length} Replies
</button>
<button className="flex items-center gap-1">
<Heart className="w-4 h-4" />
{comment.likes.length}
</button>
</div>
</div>
))}
</>
) : (
<div>
<p className="text-left">No comments yet</p>
</div>
)}
</div>

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

0 comments on commit 28f8885

Please sign in to comment.