Skip to content

Commit

Permalink
Merge pull request #138 from MeetDOD/issue-132
Browse files Browse the repository at this point in the history
feat: Implemented Leaderboard approach in style share successfully issue 132
  • Loading branch information
VaibhavArora314 authored Jun 8, 2024
2 parents d93e102 + f8e8480 commit 8d49b73
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 10 deletions.
69 changes: 68 additions & 1 deletion backend/src/routes/post/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@ export const getPostController = async (req: Request, res: Response) => {
}
};

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 getPostsWithPagination = async (req: Request, res: Response) => {
try {
const page = parseInt(req.query.page as string);
Expand Down Expand Up @@ -513,4 +536,48 @@ export const getFavoritePostsController = async (req: UserAuthRequest, res: Resp
} catch (error) {
res.status(500).json({ error: 'Failed to fetch favorite posts.' });
}
};
};

export const getLeaderboardController = async (req: Request, res: Response) => {
try {
const leaderboard = await prisma.user.findMany({
select: {
id: true,
username: true,
_count: {
select: { posts: true },
},
posts: {
select: {
likes: true,
},
},
},
});

const userLikes = leaderboard.map((user) => ({
id: user.id,
username: user.username,
postCount: user._count.posts,
totalLikes: user.posts.reduce((sum, post) => sum + post.likes, 0),
}));

userLikes.sort((a, b) => b.totalLikes - a.totalLikes);

const top10Users = userLikes.slice(0, 10);

res.status(200).json({
leaderboard: top10Users.map((user, index) => ({
rank: index + 1,
userId: user.id,
username: user.username,
postCount: user.postCount,
totalLikes: user.totalLikes,
})),
});
} catch (error) {
res.status(500).json({
error: "Failed to fetch leaderboard.",
});
}
};
6 changes: 5 additions & 1 deletion backend/src/routes/post/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@

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

const postRouter = Router();

postRouter.get('/', getPostsWithPagination);

postRouter.get('/lead', getPostController);

postRouter.post('/', authMiddleware, createPostController)

postRouter.get('/:id', getPostController);
Expand All @@ -25,4 +27,6 @@ postRouter.post('/:id/unfavorite', authMiddleware, unfavoritePostController);

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

postRouter.get('/all/leaderboard', getLeaderboardController);

export default postRouter;
2 changes: 1 addition & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
8 changes: 8 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import GoTop from "./components/GoTop";
import { Toaster } from 'react-hot-toast';
import PageNotFound from "./pages/PageNotFound";
import Favorite from "./pages/Favorite";
import LeaderBoard from "./pages/LeaderBoard";
// import axios from "axios";
// axios.defaults.baseURL = "http://localhost:3001/";

function App() {

return (
<BrowserRouter>
<RecoilRoot>
Expand Down Expand Up @@ -76,6 +78,12 @@ function App() {
</AuthenticatedRoute>
}
/>
<Route
path="/app/leaderboard"
element={
<LeaderBoard />
}
/>
<Route
path="/app/contact-us"
element={
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 @@ -80,6 +80,11 @@ const Navbar = () => {
Posts
</Link>
</li>
<li className="mt-2">
<Link to="/app/leaderboard" className={getNavLinkClass("/app/leaderboard")} onClick={closeMenu}>
Leaderboard
</Link>
</li>
{!isLoggedIn ? (
<div className="flex flex-col md:flex-row md:space-x-4">
<li className="mb-2 md:mb-0">
Expand Down
28 changes: 21 additions & 7 deletions frontend/src/pages/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,28 @@ const Comment = () => {
<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>
<ul className="space-y-3">
{comments.map((comment: IComment) => (
<li key={comment.id} className="border-b border-gray-700 pb-3 flex items-start space-x-3">
<img
src={`https://ui-avatars.com/api/?name=${comment.user?.username}&background=0ea5e9&color=fff&rounded=true&bold=true`}
width={40}
alt="profile-pic"
className="flex-shrink-0"
/>
<div>
<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>
</div>
</li>
))}
</ul>

) : (
<p className="text-gray-300">Be the first one to comment...</p>
)}
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/pages/LeaderBoard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import Loader from '../components/Loader';
import { ILeaderboardUser } from '../types';
import { GiTrophyCup } from "react-icons/gi";
import { useRecoilValue } from 'recoil';
import { userState } from '../store/atoms/auth';

const LeaderBoard = () => {
const [loading, setLoading] = useState(true);
const [leaderboard, setLeaderboard] = useState<ILeaderboardUser[]>([]);
const currentUser = useRecoilValue(userState);

useEffect(() => {
const fetchLeaderboard = async () => {
try {
const response = await axios.get('/api/v1/posts/all/leaderboard');
setLeaderboard(response.data.leaderboard);
} catch (error) {
console.error('Failed to fetch leaderboard:', error);
} finally {
setLoading(false);
}
};

fetchLeaderboard();
}, []);

return (
<div className="p-3 mb-10">
<h2 className="text-3xl font-bold text-center mb-8 text-gray-50">Leaderboard 🥳</h2>
<div className="shadow-md bg-blue-950 backdrop-blur-sm rounded-lg p-4 border-2 border-sky-500 lg:mx-52 md:mx-20 overflow-x-auto">
{loading ? (
<div className="flex justify-center">
<Loader />
</div>
) : (
<table className="min-w-full divide-y divide-gray-200">
<thead className='text-center text-sm font-medium text-gray-100 border-b-2 border-sky-600 '>
<tr>
<th scope="col" className='px-6 py-3 text-gray-100 uppercase tracking-wider'>Rank</th>
<th scope="col" className='px-6 py-3 text-gray-100 uppercase tracking-wider'>Profile</th>
<th scope="col" className='px-6 py-3 text-gray-100 uppercase tracking-wider'>Username</th>
<th scope="col" className='px-6 py-3 text-gray-100 uppercase tracking-wider'>Posts</th>
<th scope="col" className='px-6 py-3 text-gray-100 uppercase tracking-wider'>Likes</th>
</tr>
</thead>
<tbody>
{leaderboard.map((user, index) => (
<tr
key={user.userId}
className={`text-center text-gray-50 border-b-2 border-sky-900 font-semibold ${currentUser && user.userId === currentUser.id ? ' bg-sky-500' : ''}`}
>
<td className='px-6 py-4 '>
{(index === 0 || index === 1 || index === 2) ? (
<div className="flex flex-col items-center">
<GiTrophyCup className={index === 0 ? "text-[#FFD700] text-4xl" : index === 1 ? "text-[#C0C0C0] text-4xl" : "text-[#CD7F32] text-4xl"} />
</div>
) : (
user.rank
)}
</td>
<td className='px-6 py-4 '>
<div className="flex flex-col items-center">
<img className="h-8 w-8 rounded-full" src={`https://ui-avatars.com/api/?name=${user?.username}&background=0ea5e9&color=fff&rounded=true&bold=true`} alt="profile-pic" />
</div>
</td>
<td className='px-6 py-4 '>
<div className={`text-sm text-gray-50 ${currentUser && user.userId === currentUser.id ? 'font-bold' : ''}`}>@{user.username}</div>
</td>
<td className='px-6 py-4 '>
<div className="text-sm text-gray-50">{user.postCount}</div>
</td>
<td className='px-6 py-4 '>
<div className="text-sm text-gray-50">{user.totalLikes}</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}

export default LeaderBoard;
8 changes: 8 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ export interface IUser {
verified: boolean;
posts: IPost[];
favoritePosts?: IPost[];
}

export interface ILeaderboardUser {
rank: number;
userId: string;
username: string;
postCount: number;
totalLikes:number;
}

0 comments on commit 8d49b73

Please sign in to comment.