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: Added Like and Dislike features Successfully Issue 87 #102

Merged
merged 4 commits into from
Jun 4, 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
16 changes: 16 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ model User {
posts Post[] @relation("authorPosts")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
interactions UserPostInteraction[]
}

model Post {
Expand All @@ -35,4 +36,19 @@ model Post {
author User @relation("authorPosts", fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
likes Int @default(0)
dislikes Int @default(0)
interactions UserPostInteraction[]
}

model UserPostInteraction {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
postId String @db.ObjectId
liked Boolean
disliked Boolean
user User @relation(fields: [userId], references: [id])
post Post @relation(fields: [postId], references: [id])

@@unique([userId, postId])
}
136 changes: 135 additions & 1 deletion backend/src/routes/post/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const getPostController = async (req: Request, res: Response) => {
codeSnippet: true,
description: true,
tags: true,
likes:true,
dislikes:true,
author: {
select: {
id: true,
Expand Down Expand Up @@ -178,4 +180,136 @@ export const getPostsWithPagination = async (req: Request, res: Response) => {
} catch (error) {
res.status(500).json({ error: 'Failed to fetch posts' });
}
};
};

export const likePostController = async (req: UserAuthRequest, res: Response) => {
try {
const userId = req.userId;
const postId = req.params.id;

if (!userId) {
return res.status(400).json({ error: "User ID is required." });
}

const interaction = await prisma.userPostInteraction.findUnique({
where: {
userId_postId: {
userId,
postId
}
}
});

if (interaction) {
if (interaction.liked) {
return res.status(400).json({ error: "You have already liked this post." });
} else {
await prisma.userPostInteraction.update({
where: { id: interaction.id },
data: { liked: true, disliked: false }
});
await prisma.post.update({
where: { id: postId },
data: {
likes: { increment: 1 },
dislikes: interaction.disliked ? { decrement: 1 } : undefined
}
});
}
} else {
await prisma.userPostInteraction.create({
data: {
userId,
postId,
liked: true,
disliked: false
}
});
await prisma.post.update({
where: { id: postId },
data: { likes: { increment: 1 } }
});
}

const post = await prisma.post.findUnique({
where: { id: postId },
select: { likes: true, dislikes: true }
});

res.status(200).json({
message: "Post liked successfully!",
likes: post?.likes,
dislikes: post?.dislikes
});
} catch (error) {
res.status(500).json({
error: "Failed to like the post."
});
}
};

export const dislikePostController = async (req: UserAuthRequest, res: Response) => {
try {
const userId = req.userId;
const postId = req.params.id;

if (!userId) {
return res.status(400).json({ error: "User ID is required." });
}

const interaction = await prisma.userPostInteraction.findUnique({
where: {
userId_postId: {
userId,
postId
}
}
});

if (interaction) {
if (interaction.disliked) {
return res.status(400).json({ error: "You have already disliked this post." });
} else {
await prisma.userPostInteraction.update({
where: { id: interaction.id },
data: { liked: false, disliked: true }
});
await prisma.post.update({
where: { id: postId },
data: {
dislikes: { increment: 1 },
likes: interaction.liked ? { decrement: 1 } : undefined
}
});
}
} else {
await prisma.userPostInteraction.create({
data: {
userId,
postId,
liked: false,
disliked: true
}
});
await prisma.post.update({
where: { id: postId },
data: { dislikes: { increment: 1 } }
});
}

const post = await prisma.post.findUnique({
where: { id: postId },
select: { dislikes: true, likes: true }
});

res.status(200).json({
message: "Post disliked successfully!",
dislikes: post?.dislikes,
likes: post?.likes
});
} catch (error) {
res.status(500).json({
error: "Failed to dislike the post."
});
}
};
7 changes: 6 additions & 1 deletion backend/src/routes/post/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

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

const postRouter = Router();

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

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

postRouter.post('/:id/like', authMiddleware, likePostController);

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

export default postRouter;
135 changes: 111 additions & 24 deletions frontend/src/pages/Post.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router-dom";
import axios, { AxiosError } from "axios";
import { IPost } from "../types";
import DOMPurify from "dompurify";
import Loader from "../components/Loader";
import { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
import { IPost } from '../types';
import DOMPurify from 'dompurify';
import { BiDislike,BiLike,BiSolidDislike,BiSolidLike } from "react-icons/bi";
import Loader from '../components/Loader'

const Post = () => {
const { id } = useParams<{ id: string }>();
Expand All @@ -16,18 +17,21 @@ const Post = () => {
author: {
id: "",
username: "",
email: "",
email: ""
},
});
likes: 0,
dislikes: 0
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isPreview, setIsPreview] = useState(false);
const ref = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState("0px");
const [height, setHeight] = useState('0px');
const [userLiked, setUserLiked] = useState(false);
const [userDisliked, setUserDisliked] = useState(false);

const onLoad = () => {
setHeight(ref.current?.contentWindow?.document.body.scrollHeight + "px");
console.log(ref.current?.contentWindow?.document.body.scrollHeight);
setHeight(ref.current?.contentWindow?.document.body.scrollHeight + 'px');
};

useEffect(() => {
Expand All @@ -37,11 +41,8 @@ const Post = () => {
setPost(response.data.post);
setLoading(false);
} catch (error) {
const axiosError = error as AxiosError<{
error: string;
}>;

setError(axiosError.response?.data.error || "Failed to fetch the post");
const axiosError = error as AxiosError<{ error: string }>;
setError(axiosError.response?.data.error || 'Failed to fetch the post');
setLoading(false);
}
};
Expand All @@ -51,17 +52,91 @@ const Post = () => {

useEffect(() => {
onLoad();
}, [isPreview, post.codeSnippet]);
}, [isPreview, post?.codeSnippet]);

const handleCopy = () => {
navigator.clipboard.writeText(post.codeSnippet);
alert("Code snippet copied to clipboard");
if (post) {
navigator.clipboard.writeText(post.codeSnippet);
alert('Code snippet copied to clipboard');
}
};

const togglePreview = () => {
setIsPreview(!isPreview);
};

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

fetchPost();
}, [id]);

useEffect(() => {
onLoad();
}, [isPreview, post?.codeSnippet]);

useEffect(() => {
const userLikedStatus = localStorage.getItem(`post-${id}-liked`);
const userDislikedStatus = localStorage.getItem(`post-${id}-disliked`);
setUserLiked(userLikedStatus === 'true');
setUserDisliked(userDislikedStatus === 'true');
}, [id]);

const handleLike = async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
alert('You need to be logged in to like a post');
return;
}
const response = await axios.post(`/api/v1/posts/${id}/like`, {}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
setPost(prevPost => ({ ...prevPost, likes: response.data.likes, dislikes: response.data.dislikes }));
setUserLiked(true);
setUserDisliked(false);
localStorage.setItem(`post-${id}-liked`, 'true');
localStorage.removeItem(`post-${id}-disliked`);
} catch (error) {
alert('like is done only once, no spam 😊');
}
};

const handleDislike = async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
alert('You need to be logged in to dislike a post');
return;
}
const response = await axios.post(`/api/v1/posts/${id}/dislike`, {}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
setPost(prevPost => ({ ...prevPost, dislikes: response.data.dislikes, likes: response.data.likes }));
setUserLiked(false);
setUserDisliked(true);
localStorage.setItem(`post-${id}-disliked`, 'true');
localStorage.removeItem(`post-${id}-liked`);
} catch (error) {
alert('Dislike is done only once, no spam 😊');
}
};


if (loading) {
return <Loader />;
}
Expand Down Expand Up @@ -95,12 +170,24 @@ const Post = () => {
<div className="p-6 text-white max-w-screen-xl mx-auto">
{post && (
<>
<button onClick={() => window.history.back()} className="mb-4 px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<button onClick={() => window.history.back()} className="mb-4 px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
<h2 className="text-2xl font-semibold mb-4">{post.title}</h2>
<h2 className="text-2xl font-semibold mr-4">{post.title}</h2>
<button
onClick={handleLike}
className="px-4 py-2 my-3 rounded-md border-2 text-white text-sm mr-2"
>
{userLiked ? <BiSolidLike size={25} /> : <BiLike size={25} />} {post.likes}
</button>
<button
onClick={handleDislike}
className="px-4 py-2 rounded-md border-2 text-white text-sm"
>
{userDisliked ? <BiSolidDislike size={25} /> : <BiDislike size={25} />} {post.dislikes}
</button>
<p className="mb-4">{post.description}</p>
<div className="relative mb-4">
{isPreview ? (
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export interface IPost {
id: string;
username: string;
email: string;
};
},
likes: number,
dislikes: number,
}

export interface IUser {
Expand All @@ -17,4 +19,4 @@ export interface IUser {
email: string;
verified: boolean;
posts: IPost[];
}
}
Loading