Skip to content

Commit 4f94c94

Browse files
Merge pull request #102 from MeetDOD/issue-87
feat: Added Like and Dislike features Successfully Issue 87
2 parents 5e46374 + 111825a commit 4f94c94

File tree

5 files changed

+272
-28
lines changed

5 files changed

+272
-28
lines changed

backend/prisma/schema.prisma

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ model User {
2323
posts Post[] @relation("authorPosts")
2424
createdAt DateTime @default(now())
2525
updatedAt DateTime @updatedAt
26+
interactions UserPostInteraction[]
2627
}
2728

2829
model Post {
@@ -35,4 +36,19 @@ model Post {
3536
author User @relation("authorPosts", fields: [authorId], references: [id])
3637
createdAt DateTime @default(now())
3738
updatedAt DateTime @updatedAt
39+
likes Int @default(0)
40+
dislikes Int @default(0)
41+
interactions UserPostInteraction[]
3842
}
43+
44+
model UserPostInteraction {
45+
id String @id @default(auto()) @map("_id") @db.ObjectId
46+
userId String @db.ObjectId
47+
postId String @db.ObjectId
48+
liked Boolean
49+
disliked Boolean
50+
user User @relation(fields: [userId], references: [id])
51+
post Post @relation(fields: [postId], references: [id])
52+
53+
@@unique([userId, postId])
54+
}

backend/src/routes/post/controller.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export const getPostController = async (req: Request, res: Response) => {
120120
codeSnippet: true,
121121
description: true,
122122
tags: true,
123+
likes:true,
124+
dislikes:true,
123125
author: {
124126
select: {
125127
id: true,
@@ -178,4 +180,136 @@ export const getPostsWithPagination = async (req: Request, res: Response) => {
178180
} catch (error) {
179181
res.status(500).json({ error: 'Failed to fetch posts' });
180182
}
181-
};
183+
};
184+
185+
export const likePostController = async (req: UserAuthRequest, res: Response) => {
186+
try {
187+
const userId = req.userId;
188+
const postId = req.params.id;
189+
190+
if (!userId) {
191+
return res.status(400).json({ error: "User ID is required." });
192+
}
193+
194+
const interaction = await prisma.userPostInteraction.findUnique({
195+
where: {
196+
userId_postId: {
197+
userId,
198+
postId
199+
}
200+
}
201+
});
202+
203+
if (interaction) {
204+
if (interaction.liked) {
205+
return res.status(400).json({ error: "You have already liked this post." });
206+
} else {
207+
await prisma.userPostInteraction.update({
208+
where: { id: interaction.id },
209+
data: { liked: true, disliked: false }
210+
});
211+
await prisma.post.update({
212+
where: { id: postId },
213+
data: {
214+
likes: { increment: 1 },
215+
dislikes: interaction.disliked ? { decrement: 1 } : undefined
216+
}
217+
});
218+
}
219+
} else {
220+
await prisma.userPostInteraction.create({
221+
data: {
222+
userId,
223+
postId,
224+
liked: true,
225+
disliked: false
226+
}
227+
});
228+
await prisma.post.update({
229+
where: { id: postId },
230+
data: { likes: { increment: 1 } }
231+
});
232+
}
233+
234+
const post = await prisma.post.findUnique({
235+
where: { id: postId },
236+
select: { likes: true, dislikes: true }
237+
});
238+
239+
res.status(200).json({
240+
message: "Post liked successfully!",
241+
likes: post?.likes,
242+
dislikes: post?.dislikes
243+
});
244+
} catch (error) {
245+
res.status(500).json({
246+
error: "Failed to like the post."
247+
});
248+
}
249+
};
250+
251+
export const dislikePostController = async (req: UserAuthRequest, res: Response) => {
252+
try {
253+
const userId = req.userId;
254+
const postId = req.params.id;
255+
256+
if (!userId) {
257+
return res.status(400).json({ error: "User ID is required." });
258+
}
259+
260+
const interaction = await prisma.userPostInteraction.findUnique({
261+
where: {
262+
userId_postId: {
263+
userId,
264+
postId
265+
}
266+
}
267+
});
268+
269+
if (interaction) {
270+
if (interaction.disliked) {
271+
return res.status(400).json({ error: "You have already disliked this post." });
272+
} else {
273+
await prisma.userPostInteraction.update({
274+
where: { id: interaction.id },
275+
data: { liked: false, disliked: true }
276+
});
277+
await prisma.post.update({
278+
where: { id: postId },
279+
data: {
280+
dislikes: { increment: 1 },
281+
likes: interaction.liked ? { decrement: 1 } : undefined
282+
}
283+
});
284+
}
285+
} else {
286+
await prisma.userPostInteraction.create({
287+
data: {
288+
userId,
289+
postId,
290+
liked: false,
291+
disliked: true
292+
}
293+
});
294+
await prisma.post.update({
295+
where: { id: postId },
296+
data: { dislikes: { increment: 1 } }
297+
});
298+
}
299+
300+
const post = await prisma.post.findUnique({
301+
where: { id: postId },
302+
select: { dislikes: true, likes: true }
303+
});
304+
305+
res.status(200).json({
306+
message: "Post disliked successfully!",
307+
dislikes: post?.dislikes,
308+
likes: post?.likes
309+
});
310+
} catch (error) {
311+
res.status(500).json({
312+
error: "Failed to dislike the post."
313+
});
314+
}
315+
};

backend/src/routes/post/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
12
import { Router } from "express";
23
import authMiddleware from "../../middleware/auth"
3-
import { createPostController, getPostController, getPostsWithPagination } from "./controller";
4+
import { createPostController, dislikePostController, getPostController, getPostsWithPagination, likePostController } from "./controller";
45

56
const postRouter = Router();
67

@@ -10,4 +11,8 @@ postRouter.post('/', authMiddleware, createPostController)
1011

1112
postRouter.get('/:id', getPostController);
1213

14+
postRouter.post('/:id/like', authMiddleware, likePostController);
15+
16+
postRouter.post('/:id/dislike', authMiddleware, dislikePostController);
17+
1318
export default postRouter;

frontend/src/pages/Post.tsx

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useEffect, useState, useRef } from "react";
2-
import { useParams } from "react-router-dom";
3-
import axios, { AxiosError } from "axios";
4-
import { IPost } from "../types";
5-
import DOMPurify from "dompurify";
6-
import Loader from "../components/Loader";
1+
import { useEffect, useState, useRef } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import axios, { AxiosError } from 'axios';
4+
import { IPost } from '../types';
5+
import DOMPurify from 'dompurify';
6+
import { BiDislike,BiLike,BiSolidDislike,BiSolidLike } from "react-icons/bi";
7+
import Loader from '../components/Loader'
78

89
const Post = () => {
910
const { id } = useParams<{ id: string }>();
@@ -16,18 +17,21 @@ const Post = () => {
1617
author: {
1718
id: "",
1819
username: "",
19-
email: "",
20+
email: ""
2021
},
21-
});
22+
likes: 0,
23+
dislikes: 0
24+
});
2225
const [loading, setLoading] = useState(true);
2326
const [error, setError] = useState<string | null>(null);
2427
const [isPreview, setIsPreview] = useState(false);
2528
const ref = useRef<HTMLIFrameElement>(null);
26-
const [height, setHeight] = useState("0px");
29+
const [height, setHeight] = useState('0px');
30+
const [userLiked, setUserLiked] = useState(false);
31+
const [userDisliked, setUserDisliked] = useState(false);
2732

2833
const onLoad = () => {
29-
setHeight(ref.current?.contentWindow?.document.body.scrollHeight + "px");
30-
console.log(ref.current?.contentWindow?.document.body.scrollHeight);
34+
setHeight(ref.current?.contentWindow?.document.body.scrollHeight + 'px');
3135
};
3236

3337
useEffect(() => {
@@ -37,11 +41,8 @@ const Post = () => {
3741
setPost(response.data.post);
3842
setLoading(false);
3943
} catch (error) {
40-
const axiosError = error as AxiosError<{
41-
error: string;
42-
}>;
43-
44-
setError(axiosError.response?.data.error || "Failed to fetch the post");
44+
const axiosError = error as AxiosError<{ error: string }>;
45+
setError(axiosError.response?.data.error || 'Failed to fetch the post');
4546
setLoading(false);
4647
}
4748
};
@@ -51,17 +52,91 @@ const Post = () => {
5152

5253
useEffect(() => {
5354
onLoad();
54-
}, [isPreview, post.codeSnippet]);
55+
}, [isPreview, post?.codeSnippet]);
5556

5657
const handleCopy = () => {
57-
navigator.clipboard.writeText(post.codeSnippet);
58-
alert("Code snippet copied to clipboard");
58+
if (post) {
59+
navigator.clipboard.writeText(post.codeSnippet);
60+
alert('Code snippet copied to clipboard');
61+
}
5962
};
6063

6164
const togglePreview = () => {
6265
setIsPreview(!isPreview);
6366
};
6467

68+
useEffect(() => {
69+
const fetchPost = async () => {
70+
try {
71+
const response = await axios.get(`/api/v1/posts/${id}`);
72+
setPost(response.data.post);
73+
setLoading(false);
74+
} catch (error) {
75+
const axiosError = error as AxiosError<{ error: string }>;
76+
setError(axiosError.response?.data.error || 'Failed to fetch the post');
77+
setLoading(false);
78+
}
79+
};
80+
81+
fetchPost();
82+
}, [id]);
83+
84+
useEffect(() => {
85+
onLoad();
86+
}, [isPreview, post?.codeSnippet]);
87+
88+
useEffect(() => {
89+
const userLikedStatus = localStorage.getItem(`post-${id}-liked`);
90+
const userDislikedStatus = localStorage.getItem(`post-${id}-disliked`);
91+
setUserLiked(userLikedStatus === 'true');
92+
setUserDisliked(userDislikedStatus === 'true');
93+
}, [id]);
94+
95+
const handleLike = async () => {
96+
try {
97+
const token = localStorage.getItem('token');
98+
if (!token) {
99+
alert('You need to be logged in to like a post');
100+
return;
101+
}
102+
const response = await axios.post(`/api/v1/posts/${id}/like`, {}, {
103+
headers: {
104+
'Authorization': `Bearer ${token}`
105+
}
106+
});
107+
setPost(prevPost => ({ ...prevPost, likes: response.data.likes, dislikes: response.data.dislikes }));
108+
setUserLiked(true);
109+
setUserDisliked(false);
110+
localStorage.setItem(`post-${id}-liked`, 'true');
111+
localStorage.removeItem(`post-${id}-disliked`);
112+
} catch (error) {
113+
alert('like is done only once, no spam 😊');
114+
}
115+
};
116+
117+
const handleDislike = async () => {
118+
try {
119+
const token = localStorage.getItem('token');
120+
if (!token) {
121+
alert('You need to be logged in to dislike a post');
122+
return;
123+
}
124+
const response = await axios.post(`/api/v1/posts/${id}/dislike`, {}, {
125+
headers: {
126+
'Authorization': `Bearer ${token}`
127+
}
128+
});
129+
setPost(prevPost => ({ ...prevPost, dislikes: response.data.dislikes, likes: response.data.likes }));
130+
setUserLiked(false);
131+
setUserDisliked(true);
132+
localStorage.setItem(`post-${id}-disliked`, 'true');
133+
localStorage.removeItem(`post-${id}-liked`);
134+
} catch (error) {
135+
alert('Dislike is done only once, no spam 😊');
136+
}
137+
};
138+
139+
65140
if (loading) {
66141
return <Loader />;
67142
}
@@ -95,12 +170,24 @@ const Post = () => {
95170
<div className="p-6 text-white max-w-screen-xl mx-auto">
96171
{post && (
97172
<>
98-
<button onClick={() => window.history.back()} className="mb-4 px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded">
99-
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
100-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
101-
</svg>
173+
<button onClick={() => window.history.back()} className="mb-4 px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded">
174+
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
175+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
176+
</svg>
102177
</button>
103-
<h2 className="text-2xl font-semibold mb-4">{post.title}</h2>
178+
<h2 className="text-2xl font-semibold mr-4">{post.title}</h2>
179+
<button
180+
onClick={handleLike}
181+
className="px-4 py-2 my-3 rounded-md border-2 text-white text-sm mr-2"
182+
>
183+
{userLiked ? <BiSolidLike size={25} /> : <BiLike size={25} />} {post.likes}
184+
</button>
185+
<button
186+
onClick={handleDislike}
187+
className="px-4 py-2 rounded-md border-2 text-white text-sm"
188+
>
189+
{userDisliked ? <BiSolidDislike size={25} /> : <BiDislike size={25} />} {post.dislikes}
190+
</button>
104191
<p className="mb-4">{post.description}</p>
105192
<div className="relative mb-4">
106193
{isPreview ? (

frontend/src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export interface IPost {
88
id: string;
99
username: string;
1010
email: string;
11-
};
11+
},
12+
likes: number,
13+
dislikes: number,
1214
}
1315

1416
export interface IUser {
@@ -17,4 +19,4 @@ export interface IUser {
1719
email: string;
1820
verified: boolean;
1921
posts: IPost[];
20-
}
22+
}

0 commit comments

Comments
 (0)