Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Community Interaction with comment section added successfully Issue 116 #122

Merged
merged 3 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
interactions UserPostInteraction[]
comments Comment[] @relation("userComments")
}

model Post {
Expand All @@ -39,6 +40,7 @@ model Post {
likes Int @default(0)
dislikes Int @default(0)
interactions UserPostInteraction[]
comments Comment[] @relation("postComments")
}

model UserPostInteraction {
Expand All @@ -51,4 +53,15 @@ model UserPostInteraction {
post Post @relation(fields: [postId], references: [id])

@@unique([userId, postId])
}

model Comment {
id String @id @default(auto()) @map("_id") @db.ObjectId
content String
userId String @db.ObjectId
postId String @db.ObjectId
user User @relation("userComments", fields: [userId], references: [id])
post Post @relation("postComments", fields: [postId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
111 changes: 88 additions & 23 deletions backend/src/routes/post/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,6 @@ export const createPostController = async (
}
};

// export const getPostsController = async (req: Request, res: Response) => {
// const posts = await prisma.post.findMany({
// select: {
// id: true,
// title: true,
// codeSnippet: true,
// description: true,
// tags: true,
// author: {
// select: {
// id: true,
// username: true,
// email: true,
// },
// },
// },
// });

// res.status(200).json({
// posts,
// });
// };

export const getPostController = async (req: Request, res: Response) => {
try {
const postId = req.params.id;
Expand All @@ -128,6 +105,7 @@ export const getPostController = async (req: Request, res: Response) => {
username: true,
},
},
comments:true
},
});

Expand Down Expand Up @@ -313,3 +291,90 @@ export const dislikePostController = async (req: UserAuthRequest, res: Response)
});
}
};

export const createCommentController = async (req: UserAuthRequest, res: Response) => {
try {
const userId = req.userId;
const { postId } = req.params;
const { content } = req.body;

if (!userId) {
return res.status(403).json({ error: "Invalid user" });
}

const user = await prisma.user.findFirst({
where: {
id: userId,
},
select: {
verified: true,
},
});

if (!user?.verified) {
return res.status(403).json({
error: { message: "User is not verified!" },
});
}

const comment = await prisma.comment.create({
data: {
content,
userId,
postId
},
select: {
id: true,
content: true,
user: {
select: {
id: true,
username: true,
}
},
createdAt: true
}
});

res.status(201).json({
message: "Successfully created comment!",
comment,
});
} catch (error) {
res.status(500).json({
error: "An unexpected exception occurred!",
});
}
};

export const getCommentsController = async (req: Request, res: Response) => {
try {
const { postId } = req.params;

const comments = await prisma.comment.findMany({
where: { postId },
select: {
id: true,
content: true,
user: {
select: {
id: true,
username: true,
}
},
createdAt: true
},
orderBy: {
createdAt: 'desc',
}
});

res.status(200).json({
comments,
});
} catch (error) {
res.status(500).json({
error: "Failed to fetch comments",
});
}
};
6 changes: 5 additions & 1 deletion backend/src/routes/post/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { Router } from "express";
import authMiddleware from "../../middleware/auth"
import { createPostController, dislikePostController, getPostController, getPostsWithPagination, likePostController } from "./controller";
import { createCommentController, createPostController, dislikePostController, getCommentsController, getPostController, getPostsWithPagination, likePostController } from "./controller";

const postRouter = Router();

Expand All @@ -15,4 +15,8 @@ postRouter.post('/:id/like', authMiddleware, likePostController);

postRouter.post('/:id/dislike', authMiddleware, dislikePostController);

postRouter.post('/:postId/comments', authMiddleware, createCommentController);

postRouter.get('/:postId/comments', getCommentsController);

export default postRouter;
39 changes: 39 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^5.2.1",
"react-router-dom": "^6.23.1",
"react-share": "^5.1.0",
"recoil": "^0.7.7",
"tailwind-merge": "^2.3.0",
"zxcvbn": "^4.4.2"
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/pages/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
import { IComment } from '../types';
import Loader from '../components/Loader';
import toast from 'react-hot-toast';

const Comment = () => {
const { id } = useParams<{ id: string }>();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [commentContent, setCommentContent] = useState("");
const [comments, setComments] = useState<IComment[]>([]);

const fetchComments = async () => {
setLoading(true);
try {
const response = await axios.get(`/api/v1/posts/${id}/comments`);
setComments(response.data.comments);
setLoading(false);
} catch (error) {
const axiosError = error as AxiosError<{ error: string }>;
setError(axiosError.response?.data.error || 'Failed to fetch comments');
setLoading(false);
}
};

useEffect(() => {
fetchComments();
}, [id]);

const handleCommentSubmit = async (e: any) => {
e.preventDefault();
const token = localStorage.getItem('token');
if (!token) {
toast.error('Please login to comment');
return;
}
setSubmitting(true);
try {
const response = await axios.post(`/api/v1/posts/${id}/comments`, { content: commentContent }, {
headers: {
'Authorization': `Bearer ${token}`
}
});
fetchComments();
setCommentContent('');
toast.success(response.data.message);
} catch (error:any) {
if (error.response && error.response.status === 403) {
toast.error(error.response.data.error.message || 'User is not verified!');
} else {
toast.error('Failed to submit comment. Please try again later.');
}
} finally {
setSubmitting(false);
}
};


if (loading) {
return <Loader />;
}

if (error) {
return (
<div className="text-red-500 text-lg w-full text-center mt-5">
{error}
</div>
);
}

return (
<div className="">
<div>
<h3 className="text-xl font-semibold mb-4">{comments.length} Comments</h3>
{comments.length > 0 ? (
<ul className="space-y-4">
{comments.map((comment: IComment) => (
<li key={comment.id} className="border-b border-gray-700 pb-2">
<p className="text-base"><strong>@{comment.user.username} <span className='text-xs text-gray-500'> {new Date(comment.createdAt).toLocaleString()}</span></strong></p>
<p className="text-sm text-white">{comment.content}</p>
</li>
))}
</ul>
) : (
<p className="text-gray-300">Be the first one to comment...</p>
)}
</div>
<form onSubmit={handleCommentSubmit} className="mt-5">
<textarea
value={commentContent}
onChange={(e) => setCommentContent(e.target.value)}
className="w-full p-2 bg-gray-800 border border-gray-700 rounded mb-2"
rows={2}
placeholder="Add a comment..."
disabled={submitting}
required
/>
<button
type="submit"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-white"
disabled={submitting}
>
{submitting ? 'Submitting...' : 'Comment'}
</button>
</form>
{submitting && <Loader />}
</div>
);
};

export default Comment;
Loading
Loading