Skip to content

Commit

Permalink
Merge pull request #122 from MeetDOD/issue-116
Browse files Browse the repository at this point in the history
feat: Community Interaction with comment section added successfully Issue 116
  • Loading branch information
VaibhavArora314 authored Jun 5, 2024
2 parents 167ca88 + 3245eb1 commit c4fa79a
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 35 deletions.
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;
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;
19 changes: 11 additions & 8 deletions frontend/src/pages/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BiDislike,BiLike,BiSolidDislike,BiSolidLike } from "react-icons/bi";
import Loader from '../components/Loader'
import toast from 'react-hot-toast';
import { TwitterShareButton, LinkedinShareButton, FacebookShareButton, TelegramShareButton, LinkedinIcon, FacebookIcon, TelegramIcon, XIcon, WhatsappShareButton, WhatsappIcon } from 'react-share';
import Comment from './Comment';

const Post = () => {
const { id } = useParams<{ id: string }>();
Expand All @@ -22,7 +23,8 @@ const Post = () => {
email: ""
},
likes: 0,
dislikes: 0
dislikes: 0,
comments:[]
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Expand All @@ -31,6 +33,7 @@ const Post = () => {
const [height, setHeight] = useState('0px');
const [userLiked, setUserLiked] = useState(false);
const [userDisliked, setUserDisliked] = useState(false);

const shareUrl=`http://style-share.vercel.app/app/posts/${post.id}`
const title= `👋 Hey ! I found amazing tailwind css 💅 component ${post.title} have a look, The design is done by ${post.author.username} check out the link it's amazing 😀`

Expand Down Expand Up @@ -142,7 +145,6 @@ const Post = () => {
}
};


if (loading) {
return <Loader />;
}
Expand Down Expand Up @@ -195,7 +197,7 @@ const Post = () => {
{userDisliked ? <BiSolidDislike size={25} /> : <BiDislike size={25} />} {post.dislikes}
</button>
<p className="mb-4">{post.description}</p>
<div className="relative mb-4">
<div className="relative my-4">
{isPreview ? (
<div className="p-4 bg-gray-800 z-0 h-full overflow-hidden rounded border border-gray-700">
<iframe
Expand Down Expand Up @@ -248,7 +250,7 @@ const Post = () => {
</div>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold mb-2">Tags</h3>
<h3 className="text-xl font-semibold my-2">Tags</h3>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag, index) => (
<span
Expand All @@ -260,11 +262,11 @@ const Post = () => {
))}
</div>
</div>
<div className="mb-3">
<h3 className="text-xl font-semibold mb-2">Author</h3>
<p className='text-base'>Username: {post.author.username}</p>
<div className="my-5">
<h3 className="text-xl font-semibold my-2">Author</h3>
<p className='text-lg'>User: @{post.author.username}</p>
</div>
<div className="flex space-x-2">
<div className="flex space-x-2 my-4">
<TelegramShareButton url={shareUrl} title={title}>
<TelegramIcon size={35} round />
</TelegramShareButton>
Expand All @@ -281,6 +283,7 @@ const Post = () => {
<FacebookIcon size={35} round />
</FacebookShareButton>
</div>
<Comment/>
</>
)}
</div>
Expand Down
Loading

0 comments on commit c4fa79a

Please sign in to comment.