Skip to content

Commit 654e70c

Browse files
Merge pull request #182 from DevLoversTeam/feature/leaderboard-style
refactor: align leaderboard UI with brand style (fixed bg, css vars, …
2 parents 5951e3a + cfc14d0 commit 654e70c

3 files changed

Lines changed: 121 additions & 150 deletions

File tree

frontend/components/leaderboard/LeaderboardClient.tsx

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
'use client';
22

3-
import { useRef } from 'react';
4-
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
3+
import { motion } from 'framer-motion';
54
import { useTranslations } from 'next-intl';
6-
import { cn } from '@/lib/utils';
5+
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
76
import { LeaderboardPodium } from './LeaderboardPodium';
87
import { LeaderboardTable } from './LeaderboardTable';
98
import { User, CurrentUser } from './types';
109

11-
1210
interface LeaderboardClientProps {
1311
initialUsers: User[];
1412
currentUser?: CurrentUser | null;
@@ -24,75 +22,54 @@ export default function LeaderboardClient({
2422
const topThree = allUsers.filter(u => u.points > 0).slice(0, 3);
2523
const hasResults = topThree.length > 0;
2624

27-
const containerRef = useRef<HTMLDivElement>(null);
28-
const mouseX = useMotionValue(0);
29-
const mouseY = useMotionValue(0);
30-
31-
function handleMouseMove({
32-
currentTarget,
33-
clientX,
34-
clientY,
35-
}: React.MouseEvent) {
36-
mouseX.set(clientX);
37-
mouseY.set(clientY);
38-
}
39-
40-
const maskImage = useMotionTemplate`radial-gradient(500px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
41-
4225
return (
43-
<div
44-
ref={containerRef}
45-
onMouseMove={handleMouseMove}
46-
className="relative min-h-screen bg-slate-50 dark:bg-slate-950 group transition-colors duration-300"
47-
>
48-
<div
49-
className="fixed inset-0 z-0 bg-repeat opacity-60 pointer-events-none"
50-
style={{
51-
backgroundImage: `url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' stroke='%23808080' stroke-width='0.2' stroke-opacity='0.2' fill='none'/%3E%3C/svg%3E")`,
52-
backgroundSize: '40px 40px',
53-
}}
54-
/>
55-
56-
<motion.div
57-
className="fixed inset-0 z-0 bg-repeat pointer-events-none"
58-
style={{
59-
maskImage,
60-
backgroundImage: `url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' stroke='%23ff2d55' stroke-width='0.9' stroke-opacity='0.8' fill='none'/%3E%3C/svg%3E")`,
61-
backgroundSize: '40px 40px',
62-
}}
63-
/>
64-
65-
<div className="fixed top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-[#ff2d55]/10 blur-[120px] rounded-full pointer-events-none z-0" />
26+
<div className="relative min-h-screen w-full">
27+
28+
<div className="fixed inset-0 z-0">
29+
<DynamicGridBackground className="w-full h-full bg-gray-50 transition-colors duration-300 dark:bg-transparent" />
30+
</div>
6631

67-
<div className="relative max-w-5xl mx-auto px-4 py-20 flex flex-col items-center z-10">
68-
<header className="text-center mb-16 animate-in fade-in slide-in-from-top-4 duration-700">
69-
<h1 className={cn(
70-
"text-6xl md:text-8xl mb-6 uppercase select-none font-black tracking-tight",
71-
"text-[#ff2d55]",
72-
"drop-shadow-[0_0_25px_rgba(255,45,85,0.4)]"
73-
)}>
74-
{t('title')}
75-
</h1>
32+
<div className="relative z-10 w-full max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center pt-20 pb-10">
33+
34+
<header className="text-center mb-16 max-w-3xl">
35+
<motion.div
36+
initial={{ y: 20, opacity: 0 }}
37+
animate={{ y: 0, opacity: 1 }}
38+
transition={{ duration: 0.6 }}
39+
>
40+
<h1 className="text-4xl md:text-6xl lg:text-7xl font-black tracking-tight text-[var(--accent-primary)] mb-6 uppercase drop-shadow-sm">
41+
{t('title')}
42+
</h1>
43+
</motion.div>
7644

77-
<p className="text-slate-600 dark:text-slate-400 text-lg md:text-xl max-w-2xl mx-auto leading-relaxed font-medium">
45+
<motion.p
46+
initial={{ y: 20, opacity: 0 }}
47+
animate={{ y: 0, opacity: 1 }}
48+
transition={{ duration: 0.6, delay: 0.1 }}
49+
className="text-lg md:text-xl text-gray-600 dark:text-gray-400 font-light leading-relaxed"
50+
>
7851
{t('subtitle')}
79-
</p>
52+
</motion.p>
8053
</header>
8154

8255
<div className="w-full flex flex-col items-center">
83-
<div className="w-full mb-16">
56+
<div className="w-full mb-24">
8457
{hasResults ? (
8558
<LeaderboardPodium topThree={topThree} />
8659
) : (
87-
<div className="text-center py-20 rounded-2xl border border-slate-300 dark:border-white/10 bg-white/50 dark:bg-white/5 backdrop-blur-sm shadow-sm">
60+
<motion.div
61+
initial={{ opacity: 0, scale: 0.95 }}
62+
animate={{ opacity: 1, scale: 1 }}
63+
className="text-center py-20 rounded-2xl border border-gray-200 dark:border-white/10 bg-white/60 dark:bg-[#111]/60 backdrop-blur-xl shadow-xl"
64+
>
8865
<p className="text-6xl mb-4 grayscale opacity-50">🏆</p>
89-
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
66+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
9067
{t('noResults')}
9168
</h2>
92-
<p className="text-slate-600 dark:text-slate-400">
69+
<p className="text-gray-600 dark:text-gray-400">
9370
{t('beFirst')}
9471
</p>
95-
</div>
72+
</motion.div>
9673
)}
9774
</div>
9875

Lines changed: 78 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,108 @@
11
'use client';
22

33
import Image from 'next/image';
4+
import { motion } from 'framer-motion';
45
import { Crown } from 'lucide-react';
5-
import { useTranslations } from 'next-intl';
66
import { cn } from '@/lib/utils';
77
import { User } from './types';
88

99
export function LeaderboardPodium({ topThree }: { topThree: User[] }) {
10-
const t = useTranslations('leaderboard');
10+
const podiumOrder = [
11+
topThree.find(u => u.rank === 2),
12+
topThree.find(u => u.rank === 1),
13+
topThree.find(u => u.rank === 3),
14+
].filter(Boolean) as User[];
1115

1216
return (
13-
<ol
14-
className="flex items-end justify-center gap-4 md:gap-8 pb-4 pt-16 min-h-[400px] list-none m-0 p-0"
15-
aria-label={t('topThreeLabel')}
16-
>
17-
{topThree.map(user => {
18-
if (!user) return null;
17+
<div className="flex items-end justify-center gap-4 md:gap-8 h-[350px] w-full max-w-3xl mx-auto">
18+
{podiumOrder.map((user) => {
1919
const isFirst = user.rank === 1;
2020
const isSecond = user.rank === 2;
21-
const isThird = user.rank === 3;
21+
22+
const height = isFirst ? '100%' : isSecond ? '45%' : '30%';
23+
const delay = isFirst ? 0.4 : isSecond ? 0.2 : 0.6;
2224

2325
return (
24-
<li
25-
key={user.id}
26-
className={cn(
27-
'flex flex-col items-center transition-all duration-500 relative z-0',
28-
isFirst ? 'order-2 z-10 -mt-12' : '',
29-
isSecond ? 'order-1' : '',
30-
isThird ? 'order-3' : ''
31-
)}
32-
>
33-
<div className="flex flex-col items-center group w-full">
34-
<div className="relative mb-4 transition-transform duration-300 group-hover:scale-105">
26+
<div key={user.id} className="relative flex flex-col items-center justify-end w-1/3 h-full">
27+
28+
<motion.div
29+
initial={{ opacity: 0, y: 20 }}
30+
animate={{ opacity: 1, y: 0 }}
31+
transition={{ delay: delay + 0.5, duration: 0.5 }}
32+
className="mb-4 flex flex-col items-center text-center z-10"
33+
>
34+
<div className="relative mb-2">
3535
{isFirst && (
36-
<Crown
37-
className="absolute -top-12 left-1/2 -translate-x-1/2 w-10 h-10 text-yellow-400 animate-bounce drop-shadow-[0_0_15px_rgba(250,204,21,0.6)]"
38-
aria-hidden="true"
36+
<Crown
37+
className="absolute -top-8 left-1/2 -translate-x-1/2 w-6 h-6 text-[var(--accent-primary)] animate-bounce"
38+
fill="currentColor"
3939
/>
4040
)}
41-
42-
<div
43-
className={cn(
44-
'relative rounded-full p-[2px]',
45-
isFirst
46-
? 'bg-gradient-to-b from-yellow-300 to-yellow-600'
47-
: isSecond
48-
? 'bg-gradient-to-b from-slate-300 to-slate-500'
49-
: 'bg-gradient-to-b from-orange-300 to-orange-600'
50-
)}
51-
>
52-
<div className="relative w-20 h-20 md:w-24 md:h-24 rounded-full overflow-hidden border-4 border-white dark:border-slate-950 bg-slate-200 dark:bg-slate-900">
41+
<div className={cn(
42+
"relative w-16 h-16 md:w-20 md:h-20 rounded-full p-1 transition-colors duration-300",
43+
"border-2",
44+
isFirst
45+
? "border-[var(--accent-primary)]"
46+
: "border-gray-200 dark:border-white/20"
47+
)}>
48+
<div className="relative w-full h-full rounded-full overflow-hidden bg-gray-100 dark:bg-black">
5349
<Image
5450
src={user.avatar}
55-
alt={`${user.username}'s avatar`}
51+
alt={user.username}
5652
fill
5753
className="object-cover"
5854
/>
5955
</div>
60-
</div>
61-
62-
<div
63-
className={cn(
64-
'absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center justify-center w-8 h-8 rounded-full font-bold text-sm border-4 border-white dark:border-slate-950 shadow-lg',
65-
isFirst
66-
? 'bg-yellow-500 text-white'
67-
: isSecond
68-
? 'bg-slate-400 text-slate-900'
69-
: 'bg-orange-500 text-white'
70-
)}
71-
>
72-
{user.rank}
73-
</div>
74-
</div>
75-
76-
<div className="text-center mb-4">
77-
<div className="font-bold text-slate-900 dark:text-white text-sm md:text-lg mb-1 truncate max-w-[120px]">
78-
{user.username}
79-
</div>
80-
<div className="inline-block px-3 py-1 rounded-full bg-white/60 dark:bg-white/5 border border-slate-300 dark:border-white/10 backdrop-blur-sm">
81-
<div className="font-mono font-bold text-xs md:text-sm text-[#ff2d55]">
82-
{user.points}{' '}
56+
<div className={cn(
57+
"absolute -bottom-2 left-1/2 -translate-x-1/2 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-white shadow-md transition-colors duration-300",
58+
isFirst
59+
? "bg-[var(--accent-primary)]"
60+
: "bg-gray-500 dark:bg-gray-700"
61+
)}>
62+
{user.rank}
8363
</div>
8464
</div>
8565
</div>
86-
87-
<div
88-
aria-hidden="true"
89-
className={cn(
90-
'w-24 md:w-40 rounded-t-2xl backdrop-blur-md transition-all duration-300 relative overflow-hidden',
91-
'border-x border-t border-slate-300 dark:border-white/10 bg-white/60 dark:bg-white/5',
92-
93-
isFirst
94-
? 'h-48 md:h-64'
95-
: isSecond
96-
? 'h-32 md:h-44'
97-
: 'h-24 md:h-32',
98-
99-
isFirst
100-
? 'shadow-[0_0_40px_-10px_rgba(234,179,8,0.2)] dark:shadow-[0_0_40px_-10px_rgba(234,179,8,0.3)] after:absolute after:inset-0 after:bg-gradient-to-b after:from-yellow-500/10 after:to-transparent'
101-
: isSecond
102-
? 'shadow-[0_0_40px_-10px_rgba(148,163,184,0.2)] dark:shadow-[0_0_40px_-10px_rgba(148,163,184,0.2)] after:absolute after:inset-0 after:bg-gradient-to-b after:from-slate-400/10 after:to-transparent'
103-
: 'shadow-[0_0_40px_-10px_rgba(249,115,22,0.2)] dark:shadow-[0_0_40px_-10px_rgba(249,115,22,0.2)] after:absolute after:inset-0 after:bg-gradient-to-b after:from-orange-500/10 after:to-transparent'
104-
)}
105-
>
106-
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:16px_16px] opacity-50" />
66+
67+
<div className="font-bold text-gray-900 dark:text-white text-sm md:text-base truncate max-w-[100px] md:max-w-[140px]">
68+
{user.username}
69+
</div>
70+
<div className="font-mono text-xs font-bold text-[var(--accent-primary)]">
71+
{user.points}
10772
</div>
108-
</div>
109-
</li>
73+
</motion.div>
74+
75+
<motion.div
76+
initial={{ height: 0 }}
77+
animate={{ height: height }}
78+
transition={{
79+
duration: 0.8,
80+
delay: delay,
81+
type: "spring",
82+
stiffness: 60,
83+
damping: 15
84+
}}
85+
className={cn(
86+
"w-full rounded-t-2xl relative overflow-hidden backdrop-blur-xl border-x border-t transition-colors duration-300",
87+
"bg-white/60 border-gray-100 dark:bg-[#111]/60 dark:border-white/5",
88+
89+
isFirst
90+
? "shadow-[0_0_50px_-15px_var(--accent-primary)]"
91+
: "shadow-[0_-10px_40px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_-10px_40px_-15px_rgba(0,0,0,0.5)]"
92+
)}
93+
>
94+
<div className={cn(
95+
"w-full h-1.5 absolute top-0 left-0 transition-colors duration-300",
96+
isFirst
97+
? "bg-gradient-to-r from-[var(--accent-primary)] to-[var(--accent-hover)]"
98+
: "bg-gray-200 dark:bg-white/10"
99+
)} />
100+
101+
102+
</motion.div>
103+
</div>
110104
);
111105
})}
112-
</ol>
106+
</div>
113107
);
114-
}
108+
}

frontend/components/leaderboard/LeaderboardTable.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function LeaderboardTable({
7575
• • •
7676
</div>
7777

78-
<div className="bg-white dark:bg-white/5 backdrop-blur-md rounded-2xl border-2 border-[#ff2d55] overflow-hidden shadow-[0_0_20px_rgba(255,45,85,0.2)]">
78+
<div className="bg-white dark:bg-white/5 backdrop-blur-md rounded-2xl border-2 border-[var(--accent-primary)] overflow-hidden shadow-[0_0_20px_var(--accent-primary)]">
7979
<table className="w-full text-left border-collapse">
8080
<tbody>
8181
<TableRow user={matchedUser} isCurrentUser={true} t={t} />
@@ -102,7 +102,7 @@ function TableRow({
102102
className={cn(
103103
'group transition-all duration-300',
104104
isCurrentUser
105-
? 'bg-[#ff2d55]/10 dark:bg-[#ff2d55]/20 border-l-[6px] border-l-[#ff2d55] shadow-inner'
105+
? 'bg-[color-mix(in_srgb,var(--accent-primary),transparent_90%)] border-l-[6px] border-l-[var(--accent-primary)] shadow-inner'
106106
: 'hover:bg-slate-50 dark:hover:bg-white/5 border-l-[6px] border-l-transparent'
107107
)}
108108
>
@@ -118,8 +118,8 @@ function TableRow({
118118
className={cn(
119119
'w-10 h-10 rounded-full border flex items-center justify-center text-sm font-bold transition-all duration-300',
120120
isCurrentUser
121-
? 'bg-[#ff2d55] border-[#ff2d55] text-white shadow-[0_0_15px_rgba(255,45,85,0.5)]'
122-
: 'bg-slate-100 border-slate-200 text-slate-600 dark:bg-gradient-to-br dark:from-slate-800 dark:to-slate-900 dark:border-white/10 dark:text-slate-300 group-hover:border-[#ff2d55]/50 group-hover:text-[#ff2d55] dark:group-hover:text-white'
121+
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white shadow-[0_0_15px_var(--accent-primary)]'
122+
: 'bg-slate-100 border-slate-200 text-slate-600 dark:bg-gradient-to-br dark:from-slate-800 dark:to-slate-900 dark:border-white/10 dark:text-slate-300 group-hover:border-[var(--accent-primary)] group-hover:text-[var(--accent-primary)]'
123123
)}
124124
aria-hidden="true"
125125
>
@@ -131,8 +131,8 @@ function TableRow({
131131
className={cn(
132132
'font-medium text-sm transition-colors flex items-center gap-2',
133133
isCurrentUser
134-
? 'text-[#ff2d55] font-black text-base'
135-
: 'text-slate-700 dark:text-slate-200 group-hover:text-[#ff2d55] dark:group-hover:text-[#ff2d55]'
134+
? 'text-[var(--accent-primary)] font-black text-base'
135+
: 'text-slate-700 dark:text-slate-200 group-hover:text-[var(--accent-primary)]'
136136
)}
137137
>
138138
{user.username}
@@ -146,7 +146,7 @@ function TableRow({
146146
duration: 0.8,
147147
ease: 'easeInOut',
148148
}}
149-
className="absolute inset-0 text-[#ff2d55]"
149+
className="absolute inset-0 text-[var(--accent-primary)]"
150150
>
151151
<svg
152152
viewBox="0 0 24 24"
@@ -179,7 +179,7 @@ function TableRow({
179179
className={cn(
180180
'font-mono font-bold inline-block transition-all',
181181
isCurrentUser
182-
? 'text-[#ff2d55] scale-110 drop-shadow-sm text-lg'
182+
? 'text-[var(--accent-primary)] scale-110 drop-shadow-sm text-lg'
183183
: 'text-slate-700 dark:text-slate-300 group-hover:scale-105'
184184
)}
185185
>

0 commit comments

Comments
 (0)