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 leaderboard #131

Merged
merged 2 commits into from
Oct 10, 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
96 changes: 96 additions & 0 deletions app/(hacktoberfest)/leaderboard/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { LeaderboardEntry } from "@/lib/points/service";
import { Search } from "lucide-react";
import Link from "next/link";
import { Suspense, useEffect, useState } from "react";

interface ILeaderBoard {
allUserPoints: LeaderboardEntry[];
}
export default function LeaderBoardPage({ allUserPoints }: ILeaderBoard) {
return (
<Suspense fallback={<div>hang tight while we&apos;re loading the leader list...</div>}>
<LeaderBoard allUserPoints={allUserPoints} />
</Suspense>
);
}

function LeaderBoard({ allUserPoints }: ILeaderBoard) {
const [searchTerm, setSearchTerm] = useState("");
const [filteredUsers, setFilteredUsers] = useState(allUserPoints);

function findRank(user) {
let rank = allUserPoints.findIndex((originalUser) => originalUser.userId === user.userId) + 1;
return rank;
}

useEffect(() => {
// In future we can add debounce
setFilteredUsers(
allUserPoints.filter((user) => user.login.toLowerCase().includes(searchTerm.toLowerCase()))
);
}, [searchTerm, allUserPoints]);

return (
<>
<Card className="mx-auto my-12 w-full max-w-2xl overflow-hidden border-none bg-background">
<CardHeader className="p-6">
<div className="mb-4 flex items-center justify-between">
<CardTitle className="flex items-center text-base font-bold ">leaderboard 🕹️</CardTitle>
<span className="text-sm">total players: {allUserPoints.length}</span>
</div>
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 transform" />
<Input
type="text"
placeholder="search players..."
className="border-primary-foreground/20 bg-primary-foreground/10 pl-12"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-[400px] overflow-y-auto">
{filteredUsers.length === 0 ? (
<div className="py-6 text-center text-gray-500">no users found</div>
) : (
filteredUsers.map((user) => (
<div
key={user.userId}
className="flex items-center justify-between border-b px-6 py-4 transition-colors last:border-b-0 hover:bg-secondary/5">
<div className="flex items-center space-x-4">
<div className="relative">
<Avatar className="h-10 w-10 border-2 border-primary/20">
<AvatarImage src={user.avatarUrl as string} alt={user.login} />
<AvatarFallback>{user.login.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-col">
<Link
href={`/${user.login}`}
target="_blank"
className="allUserPoints transition-colors hover:text-primary hover:underline"
aria-label={`View ${user.login}'s GitHub profile`}>
<span className="text-sm">{user.login}</span>
</Link>

<span className="text-sm">rank: #{findRank(user)}</span>
</div>
</div>
<div className="flex items-center space-x-6">
<span className="text-sm">{user.totalPoints} pts</span>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
</>
);
}
8 changes: 8 additions & 0 deletions app/(hacktoberfest)/leaderboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getAllUserPointsList } from "@/lib/points/service";

import LeaderBoard from "./components";

export default async function Page() {
const allUserPoints = await getAllUserPointsList();
return <LeaderBoard allUserPoints={allUserPoints} />;
}
43 changes: 43 additions & 0 deletions lib/points/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,46 @@ export const getTotalPointsAndGlobalRank = async (userId: string) => {
likelihoodOfWinning: winProbability,
};
};

export interface LeaderboardEntry {
userId: string;
login: string;
avatarUrl: string | null;
totalPoints: number;
}


export const getAllUserPointsList = async (): Promise<LeaderboardEntry[]> => {
try {
// Fetch users and their points in a single query
const leaderboard = await db.user.findMany({
select: {
id: true,
login: true,
avatarUrl: true,
pointTransactions: {
select: {
points: true,
},
},
},
});

// Process the results
return leaderboard
.map((user) => ({
userId: user.id,
login: user.login,
avatarUrl: user.avatarUrl,
totalPoints: user.pointTransactions.reduce(
(sum, transaction) => sum + (transaction.points || 0),
0
),
}))
.sort((a, b) => b.totalPoints - a.totalPoints);
} catch (error) {
console.error('Error fetching leaderboard:', error);
throw new Error('Failed to fetch leaderboard');
}
};

Loading