Skip to content

Commit 23aa290

Browse files
committed
feat(leaderboard): progress with top users table & more hero progress
1 parent 1ad6058 commit 23aa290

File tree

6 files changed

+170
-89
lines changed

6 files changed

+170
-89
lines changed
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { getTodaysQuestion } from '@/utils/data/questions/get-today';
2-
import Hero from '@/components/global/hero';
3-
import LeaderboardLongestStreaks from '@/components/app/leaderboard/leaderboard-longest-streaks';
1+
import LeaderboardHero from '@/components/app/leaderboard/leaderboard-hero';
42
import LeaderboardMostQuestionsAnswered from '@/components/app/leaderboard/leaderboard-most-questions-answered';
5-
import LeaderboardTodayBoard from '@/components/app/leaderboard/leaderboard-today-board';
63
import { useUserServer } from '@/hooks/use-user-server';
74
import { createMetadata } from '@/utils/seo';
8-
import LeaderboardHero from '@/components/app/leaderboard/leaderboard-hero';
95
import { getMostQuestionsAnswered } from '@/utils/data/leaderboard/get-most-questions-answered';
106

117
export async function generateMetadata() {
@@ -16,24 +12,21 @@ export async function generateMetadata() {
1612
});
1713
}
1814

19-
export default async function TodaysLeaderboardPage({
20-
searchParams,
21-
}: {
22-
searchParams: { [key: string]: string | string[] | undefined };
23-
}) {
24-
const currentPage = parseInt(searchParams.page as string) || 1;
25-
26-
const topThreeUsers = await getMostQuestionsAnswered(3);
15+
export default async function TodaysLeaderboardPage() {
16+
//const currentPage = parseInt(searchParams.page as string) || 1;
2717

28-
const [user, todayQuestion] = await Promise.all([
18+
const [user, topThreeUsers] = await Promise.all([
2919
useUserServer(),
30-
getTodaysQuestion(),
20+
getMostQuestionsAnswered(3),
3121
]);
3222

3323
return (
3424
<>
25+
{/** @ts-ignore - this is the valid type */}
3526
<LeaderboardHero topThreeUsers={topThreeUsers} />
36-
<div className="lg:container flex flex-col xl:flex-row gap-10 mt-5"></div>
27+
<div className="lg:container flex flex-col xl:flex-row gap-10 mt-5">
28+
<LeaderboardMostQuestionsAnswered userUid={user?.uid} />
29+
</div>
3730
</>
3831
);
3932
}

src/components/app/leaderboard/bento-box.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ import { Separator } from '@/components/ui/separator';
55
import { Card, CardContent } from '@/components/ui/card';
66
import { ArrowRight, FlameIcon, Trophy } from 'lucide-react';
77
import Link from 'next/link';
8-
import UserRank from './user-rank';
98
import { getLongestStreaks } from '@/utils/data/leaderboard/get-longest-streaks';
109
import { getUserDisplayName } from '@/utils/user';
1110
import { Grid } from '@/components/ui/grid';
12-
import LoadingSpinner from '@/components/ui/loading';
13-
import { Suspense } from 'react';
1411

1512
export default async function TodaysLeaderboardBentoBox(opts: {
1613
todaysQuestion: Question | null;
@@ -85,9 +82,6 @@ export default async function TodaysLeaderboardBentoBox(opts: {
8582
<Separator className="bg-black-50 " />
8683
<div className="px-4 pb-4 bg-black-50/10 pt-4">
8784
<p className="text-xs text-gray-400">Your rank:</p>
88-
<Suspense fallback={<LoadingSpinner />}>
89-
<UserRank questionUid={todaysQuestion?.uid || ''} />
90-
</Suspense>
9185
</div>
9286
</div>
9387
</div>

src/components/app/leaderboard/leaderboard-hero.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { Grid } from '@/components/ui/grid';
21
import ProfilePicture from '@/components/ui/profile-picture';
32
import { UserRecord } from '@/types/User';
4-
import { shortenText } from '@/utils';
53
import { getUserDisplayName } from '@/utils/user';
64
import { Crown } from 'lucide-react';
75

@@ -21,17 +19,17 @@ export default function LeaderboardHero(opts: {
2119
const podiumOrder = [1, 0, 2]; // 2nd, 1st, 3rd
2220

2321
return (
24-
<section className="w-full py-8 flex flex-col gap-y-4 justify-between items-center">
25-
<div className="flex flex-col gap-y-3 mb-8 relative">
22+
<section className="w-full py-8 md:pb-40 flex flex-col gap-y-4 justify-between items-center">
23+
<div className="flex flex-col gap-y-3 md:mb-8 relative">
2624
<h1 className="text-3xl md:text-5xl text-wrap text-center font-inter max-w-2xl text-gradient from-white to-white/55 relative z-10">
2725
Leaderboard
2826
</h1>
29-
<p className="text-sm text-gray-400 relative z-10">
27+
<p className="text-sm text-gray-400 relative z-10 text-center">
3028
See how you stack up against the rest of the community, and try to
3129
battle your way to the top!
3230
</p>
3331
</div>
34-
<div className="flex justify-center items-end perspective-1000 relative">
32+
<div className="hidden md:flex justify-center items-end perspective-1000 relative">
3533
{podiumOrder.map((index) => {
3634
const user = topThreeUsers[index];
3735
const position = index + 1;
@@ -90,7 +88,7 @@ export default function LeaderboardHero(opts: {
9088
: 'bg-black-100'
9189
}`}
9290
style={{
93-
transform: `rotateX(78deg)`,
91+
transform: `rotateX(81deg)`,
9492
}}
9593
>
9694
{/** bottom of the podium */}
Lines changed: 106 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,122 @@
1+
import { Trophy } from 'lucide-react';
12
import { getMostQuestionsAnswered } from '@/utils/data/leaderboard/get-most-questions-answered';
2-
import Card from '@/components/global/Card';
33
import ProfilePicture from '@/components/ui/profile-picture';
44
import { UserRecord } from '@/types/User';
55
import { shortenText } from '@/utils';
66
import { getUserDisplayName } from '@/utils/user';
7-
import { Trophy } from 'lucide-react';
8-
9-
const header = () => {
10-
return (
11-
<div className="flex w-full justify-between items-center">
12-
<div className="flex flex-col gap-y-0.5">
13-
<div className="flex gap-x-2 items-center">
14-
<Trophy className="hidden md:block size-5 text-yellow-400" />
15-
<h3 className="text-lg">Top users by questions answered</h3>
16-
</div>
17-
<p className="text-xs text-gray-400">
18-
Battle your way to the top of TechBlitz!
19-
</p>
20-
</div>
21-
</div>
22-
);
23-
};
7+
import {
8+
Table,
9+
TableBody,
10+
TableCell,
11+
TableHead,
12+
TableHeader,
13+
TableRow,
14+
} from '@/components/ui/table';
15+
import {
16+
Card,
17+
CardHeader,
18+
CardTitle,
19+
CardDescription,
20+
CardContent,
21+
} from '@/components/ui/card';
22+
import { Badge } from '@/components/ui/badge';
23+
import ShowTimeTakenToggle from './show-time-taken';
2424

25-
export default async function LeaderboardMostQuestionsAnswered(opts: {
25+
export default async function LeaderboardMostQuestionsAnswered({
26+
userUid,
27+
}: {
2628
userUid?: string;
2729
}) {
28-
const { userUid } = opts;
2930
const topUsersByQuestionCount = await getMostQuestionsAnswered();
3031

3132
return (
32-
<Card header={header()}>
33-
<div className="flex flex-col divide-y-[1px] divide-black-50">
34-
{/* Headings Row */}
35-
<div className="flex items-center px-4 py-2 bg-black-75 font-medium font-ubuntu text-xs">
36-
<span className="w-[30%]">Position</span>
37-
<span className="w-[50%]">User</span>
38-
<span className="w-[20%] text-right">Answered</span>
39-
</div>
40-
41-
{topUsersByQuestionCount.map((user, index) => (
42-
<div
43-
key={user.uid}
44-
className={`flex items-center px-4 py-3 ${
45-
index % 2 === 0 ? 'bg-black' : 'bg-[#000]'
46-
}`}
47-
>
48-
{/* Position */}
49-
<span className="w-[30%]">#{index + 1}</span>
50-
51-
{/* User */}
52-
<div className="w-[50%] flex items-center gap-4">
53-
<ProfilePicture
54-
src={user.userProfilePicture}
55-
alt={`${user.username} profile picture`}
56-
/>
57-
<span>
58-
{shortenText(
59-
getUserDisplayName(user as unknown as UserRecord),
60-
25
61-
)}
62-
</span>
63-
{userUid === user.uid && (
64-
<span className="text-xs text-gray-500">(You)</span>
65-
)}
33+
<Card className="border-none">
34+
<CardHeader className="p-0 md:p-6 w-full flex gap-2 justify-between">
35+
<div className="flex flex-wrap items-center justify-between gap-2">
36+
<div className="flex items-center gap-x-2">
37+
<Trophy className="size-5 text-accent" />
38+
<div>
39+
<CardTitle className="text-white">
40+
Top Users Leaderboard
41+
</CardTitle>
42+
<CardDescription className="text-gray-400">
43+
Battle your way to the top of TechBlitz!
44+
</CardDescription>
6645
</div>
67-
68-
{/* Answered */}
69-
<span className="w-[20%] text-right">{user._count.answers}</span>
7046
</div>
71-
))}
72-
</div>
47+
<ShowTimeTakenToggle />
48+
</div>
49+
</CardHeader>
50+
<CardContent className="p-0 pt-6 md:p-6 md:pt-0">
51+
<Table>
52+
<TableHeader className="bg-transparent">
53+
<TableRow className="bg-transparent">
54+
<TableHead className="!border-t-0 w-12 md:w-[100px] text-white bg-transparent">
55+
Rank
56+
</TableHead>
57+
<TableHead className="!border-t-0 text-white bg-transparent">
58+
User
59+
</TableHead>
60+
<TableHead className="!border-t-0 text-right text-white bg-transparent">
61+
Questions Solved
62+
</TableHead>
63+
</TableRow>
64+
</TableHeader>
65+
<TableBody>
66+
{topUsersByQuestionCount.map((user, index) => (
67+
<TableRow
68+
key={user.uid}
69+
className="border-white/10 hover:bg-white/5 transition-colors"
70+
>
71+
<TableCell className="font-medium text-white">
72+
{index < 3 ? (
73+
<Badge
74+
variant={index === 0 ? 'default' : 'secondary'}
75+
className={`
76+
${index === 0 && 'bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30'}
77+
${index === 1 && 'bg-gray-400/20 text-gray-300 hover:bg-gray-400/30'}
78+
${index === 2 && 'bg-amber-700/20 text-amber-500 hover:bg-amber-700/30'}
79+
`}
80+
>
81+
#{index + 1}
82+
</Badge>
83+
) : (
84+
<span className="text-gray-400">#{index + 1}</span>
85+
)}
86+
</TableCell>
87+
<TableCell>
88+
<div className="flex items-center gap-4">
89+
<ProfilePicture
90+
src={user.userProfilePicture}
91+
alt={`${user.username} profile picture`}
92+
className="text-white"
93+
/>
94+
<div className="flex gap-2">
95+
<span className="text-white font-medium">
96+
{shortenText(
97+
getUserDisplayName(user as unknown as UserRecord),
98+
25
99+
)}
100+
</span>
101+
{userUid === user.uid && (
102+
<span className="text-xs text-white">(You)</span>
103+
)}
104+
</div>
105+
</div>
106+
</TableCell>
107+
<TableCell className="text-right">
108+
<Badge
109+
variant="outline"
110+
className="border-white/10 text-white"
111+
>
112+
{user._count.answers}
113+
</Badge>
114+
</TableCell>
115+
</TableRow>
116+
))}
117+
</TableBody>
118+
</Table>
119+
</CardContent>
73120
</Card>
74121
);
75122
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use client';
2+
3+
import { Switch } from '@/components/ui/switch';
4+
import { Label } from '@/components/ui/label';
5+
import { updateUser } from '@/actions/user/authed/update-user';
6+
import { useState } from 'react';
7+
import { toast } from 'sonner';
8+
9+
export default function ShowTimeTakenToggle() {
10+
const [checked, setChecked] = useState(true);
11+
12+
const handleSubmit = async (formData: FormData) => {
13+
const showTimeTaken = formData.get('showTimeTaken') === 'on';
14+
try {
15+
await updateUser({
16+
userDetails: {
17+
showTimeTaken,
18+
},
19+
});
20+
21+
toast.success('User updated');
22+
} catch (error) {
23+
toast.error('Failed to update user');
24+
}
25+
};
26+
27+
return (
28+
<form
29+
className="flex flex-row md:flex-col items-center md:items-end gap-2"
30+
action={handleSubmit}
31+
>
32+
<Switch
33+
id="showTimeTaken"
34+
checked={checked}
35+
onCheckedChange={(value) => {
36+
setChecked(value);
37+
// Submit the form when switch changes
38+
const formData = new FormData();
39+
formData.set('showTimeTaken', value ? 'on' : 'off');
40+
handleSubmit(formData);
41+
}}
42+
className="bg-black-50"
43+
/>
44+
<Label htmlFor="showTimeTaken" className="text-white">
45+
{checked ? 'Hide' : 'Show'} on leaderboard?
46+
</Label>
47+
</form>
48+
);
49+
}

src/utils/data/answers/get-user-answer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const getUserAnswer = async (opts: { questionUid: string }) => {
2323
return await prisma.answers.findFirst({
2424
where: {
2525
questionUid,
26-
userUid: user.uid, // user?.uid is unnecessary since we check for user existence
26+
userUid: user.uid,
2727
},
2828
});
2929
} catch (error) {

0 commit comments

Comments
 (0)