Skip to content

Commit 1a7df38

Browse files
authored
Leaderboard/redesign entire page (#406)
2 parents e3045cb + 5a48977 commit 1a7df38

File tree

12 files changed

+412
-102
lines changed

12 files changed

+412
-102
lines changed

src/app/(app)/(default_layout)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function StatisticsLayout({
77
children,
88
}: Readonly<{ children: React.ReactNode }>) {
99
return (
10-
<div className="text-white flex flex-col gap-y-4 relative h-full">
10+
<div className="text-white flex flex-col gap-y-2 relative h-full">
1111
<div className="flex w-full items-center px-6">
1212
<div className="flex-1">
1313
<SidebarLayoutTrigger />

src/app/(app)/(default_layout)/leaderboard/page.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
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';
5+
import { getMostQuestionsAnswered } from '@/utils/data/leaderboard/get-most-questions-answered';
86

97
export async function generateMetadata() {
108
return createMetadata({
@@ -14,36 +12,20 @@ export async function generateMetadata() {
1412
});
1513
}
1614

17-
export default async function TodaysLeaderboardPage({
18-
searchParams,
19-
}: {
20-
searchParams: { [key: string]: string | string[] | undefined };
21-
}) {
22-
const currentPage = parseInt(searchParams.page as string) || 1;
15+
export default async function TodaysLeaderboardPage() {
16+
//const currentPage = parseInt(searchParams.page as string) || 1;
2317

24-
const [user, todayQuestion] = await Promise.all([
18+
const [user, topThreeUsers] = await Promise.all([
2519
useUserServer(),
26-
getTodaysQuestion(),
20+
getMostQuestionsAnswered(3),
2721
]);
2822

2923
return (
3024
<>
31-
<Hero
32-
heading="Top users"
33-
subheading="See how you stack up against the rest of the community, and try to battle your way to the top!"
34-
/>
25+
{/** @ts-ignore - this is the valid type */}
26+
<LeaderboardHero topThreeUsers={topThreeUsers} />
3527
<div className="lg:container flex flex-col xl:flex-row gap-10 mt-5">
36-
<div className="w-full flex flex-col gap-10 xl:w-1/2">
37-
<LeaderboardTodayBoard
38-
todayQuestion={todayQuestion}
39-
currentPage={currentPage}
40-
userUid={user?.uid}
41-
/>
42-
<LeaderboardLongestStreaks userUid={user?.uid} />
43-
</div>
44-
<div className="w-full xl:w-1/2">
45-
<LeaderboardMostQuestionsAnswered userUid={user?.uid} />
46-
</div>
28+
<LeaderboardMostQuestionsAnswered user={user} />
4729
</div>
4830
</>
4931
);

src/app/globals.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@ html {
2222
}
2323

2424
@layer utilities {
25+
.perspective-1000 {
26+
perspective: 1000px;
27+
}
28+
29+
.transform-3d {
30+
transform-style: preserve-3d;
31+
}
32+
33+
.rotate-x-55 {
34+
transform: rotateX(55deg);
35+
}
36+
37+
.rotate-y-45 {
38+
transform: rotateY(45deg);
39+
}
40+
41+
.rotate-x-90 {
42+
transform: rotateX(90deg);
43+
}
44+
45+
.rotate-y-90 {
46+
transform: rotateY(90deg);
47+
}
48+
2549
::selection {
2650
@apply bg-accent text-white;
2751
}

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>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
'use client';
2+
import AnimatedSpan from '@/components/ui/animated-span';
3+
import ProfilePicture from '@/components/ui/profile-picture';
4+
import { UserRecord } from '@/types/User';
5+
import { getUserDisplayName } from '@/utils/user';
6+
import { motion } from 'framer-motion';
7+
import { Crown } from 'lucide-react';
8+
9+
/**
10+
* This will show the top three users on TechBlitz in a podium style with a 3D isometric look.
11+
*
12+
* @returns
13+
*/
14+
export default function LeaderboardHero(opts: {
15+
topThreeUsers: UserRecord[] &
16+
{
17+
_count: { answers: number };
18+
}[];
19+
}) {
20+
const { topThreeUsers } = opts;
21+
22+
const podiumOrder = [1, 0, 2]; // 2nd, 1st, 3rd
23+
24+
return (
25+
<section className="w-full py-8 md:pb-28 flex flex-col gap-y-4 justify-between items-center">
26+
<div className="flex flex-col gap-y-3 md:mb-8 relative items-center">
27+
<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">
28+
Leaderboard
29+
</h1>
30+
<p className="text-sm text-gray-400 relative z-10 text-center">
31+
See how you stack up against the rest of the community and become the
32+
best!
33+
</p>
34+
<div className="hidden md:flex">
35+
<AnimatedSpan content="Top Users" />
36+
</div>
37+
</div>
38+
<div className="hidden md:flex justify-center items-end perspective-1000 relative">
39+
{podiumOrder.map((index) => {
40+
const user = topThreeUsers[index];
41+
const position = index + 1;
42+
return (
43+
<motion.div
44+
key={user.uid}
45+
initial={{ opacity: 0, y: 50 }}
46+
animate={{ opacity: 1, y: 0 }}
47+
transition={{
48+
duration: 0.5,
49+
delay: position * 0.3,
50+
type: 'spring',
51+
stiffness: 100,
52+
}}
53+
className={`flex flex-col items-center ${
54+
position === 1
55+
? 'order-2'
56+
: position === 2
57+
? 'order-1'
58+
: 'order-3'
59+
}`}
60+
>
61+
<div
62+
className={`relative ${
63+
position === 1
64+
? 'size-28 md:size-64'
65+
: position === 2
66+
? 'size-24 md:h-48 md:w-64'
67+
: 'size-24 md:h-40 md:w-64'
68+
}`}
69+
>
70+
<motion.div
71+
className={`flex flex-col items-center mb-2 md:mb-4 z-20 relative ${
72+
position === 3 ? '-top-4' : ''
73+
}`}
74+
initial={{ scale: 0.8, opacity: 0 }}
75+
animate={{ scale: 1, opacity: 1 }}
76+
transition={{
77+
duration: 0.4,
78+
delay: position * 0.2,
79+
type: 'spring',
80+
bounce: 0.2,
81+
}}
82+
>
83+
<motion.div
84+
whileHover={{ scale: 1.05 }}
85+
whileTap={{ scale: 0.98 }}
86+
className="relative"
87+
>
88+
<ProfilePicture
89+
src={user.userProfilePicture}
90+
alt={`${user.username} profile picture`}
91+
className="size-8 md:size-12 rounded-full shadow-lg"
92+
/>
93+
{position === 1 && (
94+
<motion.div
95+
className="absolute -top-3 left-4 size-4"
96+
initial={{ y: -5, opacity: 0 }}
97+
animate={{ y: 0, opacity: 1 }}
98+
transition={{
99+
delay: 0.8,
100+
type: 'spring',
101+
stiffness: 200,
102+
damping: 15,
103+
}}
104+
>
105+
<Crown className="size-4 text-yellow-500 fill-yellow-400" />
106+
</motion.div>
107+
)}
108+
{user.userLevel === 'PREMIUM' && (
109+
<motion.div
110+
initial={{ x: -5, opacity: 0 }}
111+
animate={{ x: 0, opacity: 1 }}
112+
transition={{ delay: position * 0.2 + 0.3 }}
113+
className="relative -top-3 left-1.5 w-fit bg-accent text-xs flex items-center justify-center px-2 py-0.5 rounded-full"
114+
>
115+
<span className="text-[10px]">PRO</span>
116+
</motion.div>
117+
)}
118+
{user.userLevel === 'ADMIN' && (
119+
<motion.div
120+
initial={{ x: -5, opacity: 0 }}
121+
animate={{ x: 0, opacity: 1 }}
122+
transition={{ delay: position * 0.2 + 0.3 }}
123+
className="relative -top-3 w-fit bg-accent text-xs flex items-center justify-center px-2 py-0.5 rounded-full"
124+
>
125+
<span className="text-[10px]">ADMIN</span>
126+
</motion.div>
127+
)}
128+
</motion.div>
129+
<motion.span
130+
initial={{ y: 5, opacity: 0 }}
131+
animate={{ y: 0, opacity: 1 }}
132+
transition={{ delay: position * 0.2 + 0.2 }}
133+
className="text-sm md:text-base font-semibold line-clamp-1"
134+
>
135+
{getUserDisplayName(user as unknown as UserRecord)}
136+
</motion.span>
137+
<motion.span
138+
initial={{ y: 5, opacity: 0 }}
139+
animate={{ y: 0, opacity: 1 }}
140+
transition={{ delay: position * 0.2 + 0.3 }}
141+
className="text-sm text-white"
142+
>
143+
{user._count.answers} answers
144+
</motion.span>
145+
</motion.div>
146+
{/** top of the podium */}
147+
<div
148+
className={`absolute inset-0 transform-3d rotate-x-55 rotate-y-45 ${
149+
position === 1
150+
? 'bg-[#383737]'
151+
: position === 2
152+
? 'bg-black-100'
153+
: 'bg-black-100'
154+
}`}
155+
style={{
156+
transform: `rotateX(81deg)`,
157+
}}
158+
>
159+
{/** bottom of the podium */}
160+
<div
161+
className={`absolute left-0 right-0 bottom-0 h-full origin-bottom rotate-x-90 ${
162+
position === 1
163+
? 'bg-black-50'
164+
: position === 2
165+
? 'bg-black-200'
166+
: 'bg-black-200'
167+
}`}
168+
>
169+
<div
170+
className="absolute inset-0 flex items-center justify-center rotate-180"
171+
style={{
172+
transform: `rotateX(180deg)`,
173+
}}
174+
>
175+
<span className="text-2xl md:text-5xl font-bold text-gradient from-white to-white/55 font-onest">
176+
{position}
177+
{position === 1 ? 'st' : position === 2 ? 'nd' : 'rd'}
178+
</span>
179+
</div>
180+
{/** bottom fade */}
181+
<div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-[#000] to-transparent rotate-180 -top-px"></div>
182+
</div>
183+
<div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-[#000] to-transparent rotate-180 -top-px"></div>
184+
</div>
185+
</div>
186+
</motion.div>
187+
);
188+
})}
189+
</div>
190+
</section>
191+
);
192+
}

0 commit comments

Comments
 (0)