Skip to content

Commit

Permalink
Merge pull request #125 from MeetDOD/issue-123
Browse files Browse the repository at this point in the history
feat: Added mark components as favorites and unfavorites feature successfully issue 123
  • Loading branch information
VaibhavArora314 authored Jun 6, 2024
2 parents 54fe9ca + 9c3583c commit 4df3f46
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 9 deletions.
12 changes: 12 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ model User {
updatedAt DateTime @updatedAt
interactions UserPostInteraction[]
comments Comment[] @relation("userComments")
favorites Favorite[] @relation("userFavorites")
}

model Post {
Expand All @@ -41,6 +42,7 @@ model Post {
dislikes Int @default(0)
interactions UserPostInteraction[]
comments Comment[] @relation("postComments")
favorites Favorite[] @relation("postFavorites")
}

model UserPostInteraction {
Expand All @@ -64,4 +66,14 @@ model Comment {
post Post @relation("postComments", fields: [postId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Favorite {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
postId String @db.ObjectId
user User @relation("userFavorites", fields: [userId], references: [id])
post Post @relation("postFavorites", fields: [postId], references: [id])
@@unique([userId, postId])
}
136 changes: 136 additions & 0 deletions backend/src/routes/post/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,139 @@ export const getCommentsController = async (req: Request, res: Response) => {
});
}
};

export const favoritePostController = 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 user = await prisma.user.findFirst({
where: { id: userId },
select: { verified: true }
});

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

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

if (favorite) {
return res.status(400).json({ error: "You have already favorited this post." });
}

await prisma.favorite.create({
data: {
userId,
postId
}
});

res.status(200).json({ message: "Post favorited successfully" });
} catch (error) {
res.status(500).json({ error: "Failed to favorite the post." });
}
};

export const unfavoritePostController = 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 user = await prisma.user.findFirst({
where: { id: userId },
select: { verified: true }
});

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

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

if (!favorite) {
return res.status(400).json({ error: "You have not favorited this post." });
}

await prisma.favorite.delete({
where: {
userId_postId: {
userId,
postId
}
}
});

res.status(200).json({ message: "Post unfavorited successfully" });
} catch (error) {
res.status(500).json({ error: "Failed to unfavorite the post." });
}
};

export const getFavoritePostsController = async (req: UserAuthRequest, res: Response) => {
try {
const userId = req.userId;

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

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

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

const favorites = await prisma.favorite.findMany({
where: { userId },
include: {
post: {
select: {
id: true,
title: true,
codeSnippet: true,
description: true,
tags: true,
author: {
select: {
id: true,
username: true,
},
},
},
},
},
});

const favoritePosts = favorites.map((fav) => fav.post);

res.status(200).json({ favoritePosts });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch favorite posts.' });
}
};
8 changes: 7 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 { createCommentController, createPostController, dislikePostController, getCommentsController, getPostController, getPostsWithPagination, likePostController } from "./controller";
import { createCommentController, createPostController, dislikePostController, favoritePostController, getCommentsController, getFavoritePostsController, getPostController, getPostsWithPagination, likePostController, unfavoritePostController } from "./controller";

const postRouter = Router();

Expand All @@ -19,4 +19,10 @@ postRouter.post('/:postId/comments', authMiddleware, createCommentController);

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

postRouter.post('/:id/favorite', authMiddleware, favoritePostController);

postRouter.post('/:id/unfavorite', authMiddleware, unfavoritePostController);

postRouter.get('/:id/favorites', authMiddleware, getFavoritePostsController);

export default postRouter;
10 changes: 9 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Policy from "./pages/Policy";
import GoTop from "./components/GoTop";
import { Toaster } from 'react-hot-toast';
import PageNotFound from "./pages/PageNotFound";
import Favorite from "./pages/Favorite";
// import axios from "axios";
// axios.defaults.baseURL = "http://localhost:3001/";

Expand Down Expand Up @@ -67,6 +68,14 @@ function App() {
</AuthenticatedRoute>
}
/>
<Route
path="/app/fav"
element={
<AuthenticatedRoute>
<Favorite />
</AuthenticatedRoute>
}
/>
<Route
path="/app/contact-us"
element={
Expand All @@ -88,7 +97,6 @@ function App() {
<Route path="*" element={<PageNotFound/>} />
</Routes>
</div>

<Footer />
</React.Suspense>
</RecoilRoot>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ const Navbar = () => {
Profile
</Link>
</li>
<li className="mt-2">
<Link to="/app/fav" className={getNavLinkClass("/app/fav")} onClick={closeMenu}>
Favorite
</Link>
</li>
<li className="mt-2">
<button
className="block py-2 px-3 rounded md:border-0 md:p-0 text-white md:hover:text-blue-500 hover:bg-gray-700 hover:text-white md:hover:bg-transparent w-full text-left"
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/pages/Favorite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect, useState } from "react";
import { IPost, IUser } from "../types";
import axios from "axios";
import { useRecoilValue } from "recoil";
import { tokenState, userState } from "../store/atoms/auth";
import Loader from "../components/Loader";
import PostCard from "../components/PostCard";

const Favorite = () => {
const user = useRecoilValue(userState);
const [favoritePosts, setFavoritePosts] = useState<IPost[]>([]);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const token = useRecoilValue(tokenState);

const fetchFavoritePosts = async (user: IUser): Promise<IPost[]> => {
try {
const response = await axios.get(`/api/v1/posts/${user.id}/favorites`, {
headers: {
Authorization: `Bearer ${token}`,
},
});

return response.data.favoritePosts;
} catch (error) {
console.error('Error fetching favorite posts:', error);
throw new Error('Could not fetch favorite posts');
}
};

useEffect(() => {
const getFavoritePosts = async () => {
if (user && user.id) {
try {
const posts = await fetchFavoritePosts(user);
setFavoritePosts(posts);
} catch (error) {
setErrorMessage('Please verify your account to access this feature 😔 !');
} finally {
setLoading(false);
}
} else {
setLoading(false);
}
};

getFavoritePosts();
}, [user]);

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

if (errorMessage) {
return <div className='text-red-500 font-semibold text-xl text-center my-10'>{errorMessage}</div>;
}

return (
<>
<div className="max-w-screen-xl mx-auto p-4 text-white flex flex-col items-center">
<div className="w-80 bg-blue-950 backdrop-blur-sm rounded-xl p-3 border border-sky-500 text-center text-xl font-semibold">
My Favorite Posts 😍
</div>
<div className="mt-8 w-full">
{favoritePosts.length > 0 ? (
<>
<h4 className="font-semibold">Favorite Posts ( {favoritePosts.length} )</h4>
<div className="mt-6 mb-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 w-full">
{favoritePosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</>
) : (
<div className="text-center text-lg text-gray-300 font-semibold">No favorite post yet 😟</div>
)}
</div>
</div>
</>
);
};

export default Favorite;
Loading

0 comments on commit 4df3f46

Please sign in to comment.