diff --git a/src/components/LeaderboardSkeleton.tsx b/src/components/LeaderboardSkeleton.tsx new file mode 100644 index 0000000..a47e49e --- /dev/null +++ b/src/components/LeaderboardSkeleton.tsx @@ -0,0 +1,22 @@ +export const LeaderboardRowSkeleton = () => { + return ( +
+
+ {/* Rank / Trophy */} +
+ + {/* Avatar */} +
+ + {/* Name */} +
+
+ + {/* XP */} +
+
+
+
+
+ ); +}; diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx index 71b0447..52b9712 100644 --- a/src/pages/student/Practice.tsx +++ b/src/pages/student/Practice.tsx @@ -22,7 +22,7 @@ import { useNavigate } from "react-router-dom"; export const Practice = () => { const navigate = useNavigate(); return ( -
+
diff --git a/src/pages/student/Rewards.tsx b/src/pages/student/Rewards.tsx index d2eb123..55834e9 100644 --- a/src/pages/student/Rewards.tsx +++ b/src/pages/student/Rewards.tsx @@ -3,7 +3,7 @@ import firstTrophy from "../../assets/icons/first_trophy.png"; import secondTrophy from "../../assets/icons/second_trophy.png"; import thirdTrophy from "../../assets/icons/third_trophy.png"; -import { useState } from "react"; +import { useEffect, useState } from "react"; // import { // Card, // CardHeader, @@ -27,34 +27,81 @@ import { DropdownMenuTrigger, } from "../../components/ui/dropdown-menu"; import { formatTimeFilter, getRandomColor } from "../../lib/utils"; -import { Avatar, AvatarFallback } from "../../components/ui/avatar"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "../../components/ui/avatar"; import { Zap } from "lucide-react"; +import type { Leaderboard } from "../../types/leaderboard"; +import { api } from "../../utils/api"; +import { Card, CardContent } from "../../components/ui/card"; +import { LeaderboardRowSkeleton } from "../../components/LeaderboardSkeleton"; export const Rewards = () => { const user = useAuthStore((state) => state.user); const [time, setTime] = useState("bottom"); - const leaderboard = [ - { id: 1, name: "Alice", xp: 587 }, - { id: 2, name: "Bob", xp: 560 }, - { id: 3, name: "Charlie", xp: 540 }, - { id: 4, name: "David", xp: 510 }, - { id: 5, name: "Emma", xp: 495 }, - ]; + const [leaderboard, setLeaderboard] = useState(); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchLeaderboard = async () => { + if (!user) return; + + try { + setLoading(true); + const authStorage = localStorage.getItem("auth-storage"); + if (!authStorage) return; + + const parsed = JSON.parse(authStorage) as { + state?: { token?: string }; + }; + + const token = parsed.state?.token; + if (!token) return; + + const response = await api.fetchLeaderboard(token); + + setLeaderboard(response); + setLoading(false); + } catch (error) { + setLoading(false); + console.error("Error fetching leaderboard: " + error); + } + }; + + fetchLeaderboard(); + }, [user]); + const trophies = [firstTrophy, secondTrophy, thirdTrophy]; + const isTopThree = (leaderboard?.user_rank?.rank ?? Infinity) < 3; + return ( -
+

Leaderboards

-

- Complete lessons to rise to the top.{" "} - Start a lesson. -

+ {loading ? ( +
+
+
+ ) : ( +

+ Don't stop now! You're{" "} + + #{leaderboard?.user_rank.rank} + {" "} + in XP. +

+ )}
-
- - +
+ + { - - {leaderboard.map((user, index) => { - const isTopThree = index < 3; + + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : ( + leaderboard?.top_users.map((user, index) => { + const isTopThree = index < 3; - return ( -
-
- {isTopThree ? ( - {`trophy_${index - ) : ( - - {index + 1} - - )} + return ( +
+
+ {isTopThree ? ( + {`trophy_${index + ) : ( + + {index + 1} + + )} - - - {user.name.slice(0, 1).toUpperCase()} - - + + + + {user.name.slice(0, 1).toUpperCase()} + + -

- {user.name} -

+

+ {user.name} +

+
+ +
+

{user.total_xp}

+ +
- -
-

{user.xp}

- -
-
- ); - })} + ); + }) + )} - - {leaderboard.map((user, index) => { + + {/* {leaderboard.map((user, index) => { const isTopThree = index < 3; return ( @@ -180,10 +242,13 @@ export const Rewards = () => {
); - })} + })} */} - - {leaderboard.map((user, index) => { + + {/* {leaderboard.map((user, index) => { const isTopThree = index < 3; return ( @@ -221,10 +286,66 @@ export const Rewards = () => {
); - })} + })} */} + + + {loading ? ( +
+
+ {/* Rank / Trophy */} +
+ + {/* Avatar */} +
+ + {/* Name */} +
+
+ + {/* XP */} +
+
+
+
+
+ ) : ( + <> +
+ {isTopThree ? ( + {`trophy_${leaderboard?.user_rank?.rank + ) : ( + + {leaderboard?.user_rank?.rank ?? Infinity} + + )} + + + + {leaderboard?.user_rank.name.slice(0, 1).toUpperCase()} + + +

+ {leaderboard?.user_rank.name} +

+
+ +
+

+ {leaderboard?.user_rank.total_xp} +

+ +
+ + )} + + ); }; diff --git a/src/types/leaderboard.ts b/src/types/leaderboard.ts new file mode 100644 index 0000000..99b2081 --- /dev/null +++ b/src/types/leaderboard.ts @@ -0,0 +1,13 @@ +export type LeaderboardEntry = { + rank: number; + user_id: string; + name: string; + avatar_url: string; + total_xp: number; + current_level: number; +}; + +export interface Leaderboard { + top_users: LeaderboardEntry[]; + user_rank: LeaderboardEntry; +} diff --git a/src/utils/api.ts b/src/utils/api.ts index ae119f2..45d5ef7 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,3 +1,4 @@ +import type { Leaderboard } from "../types/leaderboard"; import type { Lesson, LessonsResponse } from "../types/lesson"; import type { SessionAnswerResponse, @@ -222,5 +223,9 @@ class ApiClient { async fetchTopicById(token: string, topicId: string): Promise { return this.authenticatedRequest(`/topics/${topicId}`, token); } + + async fetchLeaderboard(token: string): Promise { + return this.authenticatedRequest(`/leaderboard/`, token); + } } export const api = new ApiClient(API_URL);