Skip to content

Commit 49ce7a3

Browse files
Merge pull request #165 from DevLoversTeam/feature/accessibility-audit
2 parents 9a22b46 + 3d86d62 commit 49ce7a3

11 files changed

Lines changed: 279 additions & 158 deletions

File tree

frontend/app/[locale]/leaderboard/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { getLeaderboardData } from '@/db/queries/leaderboard';
22
import LeaderboardClient from '@/components/leaderboard/LeaderboardClient';
3+
import { Metadata } from 'next';
4+
5+
export const metadata: Metadata = {
6+
title: 'Leaderboard | DevLovers',
7+
description: 'Top performers of the community',
8+
};
39

410
export const revalidate = 3600;
511

frontend/components/header/AppMobileMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function AppMobileMenu({
5454
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
5555
aria-label="Toggle menu"
5656
aria-expanded={open ? 'true' : 'false'}
57-
aria-controls="app-mobile-nav"
57+
aria-controls={open ? 'app-mobile-nav' : undefined}
5858
>
5959
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
6060
</button>
Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
'use client';
22

3-
import { useState } from 'react';
4-
import { LeaderboardTabs } from './LeaderboardTabs';
3+
import { useTranslations } from 'next-intl';
54
import { LeaderboardPodium } from './LeaderboardPodium';
65
import { LeaderboardTable } from './LeaderboardTable';
76
import { User } from './types';
8-
import { Trophy } from 'lucide-react';
97

108
interface LeaderboardClientProps {
119
initialUsers: User[];
@@ -14,7 +12,7 @@ interface LeaderboardClientProps {
1412
export default function LeaderboardClient({
1513
initialUsers,
1614
}: LeaderboardClientProps) {
17-
const [activeTab, setActiveTab] = useState('Overall');
15+
const t = useTranslations('leaderboard');
1816

1917
const usersWithPoints = initialUsers.filter(user => user.points > 0);
2018
const hasResults = usersWithPoints.length > 0;
@@ -32,60 +30,60 @@ export default function LeaderboardClient({
3230
className="pointer-events-none absolute inset-0 opacity-70 -z-10"
3331
aria-hidden="true"
3432
>
35-
<div className="absolute -top-32 left-1/2 h-96 w-[36rem] -translate-x-1/2 rounded-full bg-sky-300/30 blur-3xl dark:bg-sky-500/20" />
36-
<div className="absolute bottom-[-12rem] left-1/4 h-[22rem] w-[22rem] rounded-full bg-pink-300/30 blur-3xl dark:bg-fuchsia-500/20" />
37-
<div className="absolute bottom-[-10rem] right-0 h-[26rem] w-[26rem] rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-500/20" />
33+
<div className="absolute -top-32 -left-32 w-96 h-96 bg-purple-200 dark:bg-purple-900/20 rounded-full blur-3xl opacity-50 animate-blob" />
34+
<div className="absolute top-1/4 -right-32 w-80 h-80 bg-sky-200 dark:bg-sky-900/20 rounded-full blur-3xl opacity-50 animate-blob animation-delay-2000" />
35+
<div className="absolute -bottom-32 left-1/3 w-96 h-96 bg-pink-200 dark:bg-pink-900/20 rounded-full blur-3xl opacity-50 animate-blob animation-delay-4000" />
3836
</div>
3937

40-
<main className="relative max-w-4xl mx-auto px-4 py-12 flex flex-col items-center z-10">
41-
<header className="mb-10 text-center">
42-
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight flex items-center justify-center gap-3 mb-4 drop-shadow-sm">
43-
<Trophy
44-
className="w-10 h-10 md:w-12 md:h-12 text-yellow-500 fill-yellow-500"
45-
strokeWidth={1.5}
46-
aria-hidden="true"
47-
/>
48-
<span className="bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent">
49-
Leaderboard
38+
<div className="relative max-w-4xl mx-auto px-4 py-12 flex flex-col items-center z-10">
39+
<header className="text-center mb-10 animate-in fade-in slide-in-from-top-4 duration-700">
40+
<div className="inline-flex items-center justify-center p-3 mb-6 rounded-full bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm shadow-sm ring-1 ring-slate-200/50 dark:ring-slate-700/50">
41+
<span className="text-2xl mr-2" role="img" aria-label="Trophy">
42+
🏆
43+
</span>
44+
<span className="text-sm font-semibold text-slate-600 dark:text-slate-300 uppercase tracking-wider">
45+
{t('championsArena')}
46+
</span>
47+
</div>
48+
49+
<h1 className="text-5xl md:text-7xl font-black mb-6 tracking-tighter drop-shadow-sm">
50+
<span className="bg-gradient-to-r from-sky-500 via-indigo-500 to-pink-500 bg-clip-text text-transparent">
51+
{t('title')}
5052
</span>
5153
</h1>
52-
<p className="text-slate-600 dark:text-slate-400 font-medium text-lg">
53-
Top performers of the community.
54+
55+
<p className="text-slate-600 dark:text-slate-400 font-medium text-lg max-w-md mx-auto leading-relaxed">
56+
{t('subtitle')}
5457
</p>
5558
</header>
5659

57-
<section className="mb-10 w-full flex justify-center">
58-
<LeaderboardTabs activeTab={activeTab} onTabChange={setActiveTab} />
59-
</section>
60-
61-
<section
62-
className="w-full flex flex-col items-center"
63-
role="tabpanel"
64-
aria-label={`Results for ${activeTab}`}
65-
>
66-
<div className="w-full mb-12">
60+
<div className="w-full flex flex-col items-center">
61+
<div className="w-full mb-16">
6762
{hasResults ? (
6863
<LeaderboardPodium topThree={topThree} />
6964
) : (
7065
<div className="text-center py-16" role="status">
71-
<p className="text-6xl mb-4" aria-hidden="true">
66+
<p
67+
className="text-6xl mb-4 grayscale opacity-50"
68+
aria-hidden="true"
69+
>
7270
🏆
7371
</p>
7472
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2">
75-
No results yet
73+
{t('noResults')}
7674
</h2>
7775
<p className="text-slate-600 dark:text-slate-400">
78-
Be the first to complete a quiz and claim the top spot!
76+
{t('beFirst')}
7977
</p>
8078
</div>
8179
)}
8280
</div>
8381

84-
<div className="w-full animate-in fade-in slide-in-from-bottom-8 duration-700">
85-
<LeaderboardTable users={otherUsers} />
82+
<div className="w-full animate-in fade-in slide-in-from-bottom-8 duration-700 delay-200">
83+
{hasResults && <LeaderboardTable users={otherUsers} />}
8684
</div>
87-
</section>
88-
</main>
85+
</div>
86+
</div>
8987
</div>
9088
);
9189
}

frontend/components/leaderboard/LeaderboardPodium.tsx

Lines changed: 41 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
import Image from 'next/image';
44
import { Crown } from 'lucide-react';
5+
import { useTranslations } from 'next-intl';
56
import { cn } from '@/lib/utils';
67
import { User } from './types';
78

89
export function LeaderboardPodium({ topThree }: { topThree: User[] }) {
10+
const t = useTranslations('leaderboard');
11+
912
return (
1013
<ol
11-
className="flex items-end justify-center gap-4 md:gap-8 pb-4 pt-8 min-h-[350px] list-none m-0 p-0"
12-
aria-label="Top 3 Leaders"
14+
className="flex items-end justify-center gap-2 md:gap-8 pb-4 pt-16 min-h-[340px] list-none m-0 p-0"
15+
aria-label={t('topThreeLabel')}
1316
>
1417
{topThree.map(user => {
1518
if (!user) return null;
@@ -21,95 +24,81 @@ export function LeaderboardPodium({ topThree }: { topThree: User[] }) {
2124
<li
2225
key={user.id}
2326
className={cn(
24-
'flex flex-col items-center transition-all duration-500 relative',
25-
26-
isFirst ? 'order-2 z-10 -mt-8' : 'z-0',
27+
'flex flex-col items-center transition-all duration-500 relative z-0',
28+
isFirst ? 'order-2 z-10 -mt-8 md:-mt-12' : '',
2729
isSecond ? 'order-1' : '',
2830
isThird ? 'order-3' : ''
2931
)}
3032
>
31-
<article className="flex flex-col items-center">
32-
<div className="relative mb-4 group cursor-pointer">
33+
<div className="flex flex-col items-center group">
34+
<div className="relative mb-3 md:mb-5 transition-transform duration-300 group-hover:scale-105">
3335
{isFirst && (
3436
<Crown
35-
className="absolute -top-12 left-1/2 -translate-x-1/2 w-10 h-10 text-yellow-500 fill-yellow-500 animate-bounce drop-shadow-lg"
36-
strokeWidth={1.5}
37+
className="absolute -top-10 md:-top-12 left-1/2 -translate-x-1/2 w-10 h-10 md:w-12 md:h-12 text-yellow-400 animate-bounce drop-shadow-[0_0_15px_rgba(250,204,21,0.5)]"
3738
aria-hidden="true"
3839
/>
3940
)}
4041

4142
<div
4243
className={cn(
43-
'relative flex items-center justify-center rounded-full overflow-hidden shadow-2xl transition-transform duration-300 group-hover:scale-105 border-4',
44+
'relative rounded-full p-[3px] shadow-2xl',
4445
isFirst
45-
? 'w-28 h-28 border-yellow-400 dark:border-yellow-500'
46-
: '',
47-
isSecond
48-
? 'w-20 h-20 border-slate-300 dark:border-slate-500'
49-
: '',
50-
isThird
51-
? 'w-20 h-20 border-orange-300 dark:border-orange-400'
52-
: '',
53-
'bg-white dark:bg-slate-800'
46+
? 'bg-gradient-to-tr from-yellow-300 via-yellow-100 to-yellow-500'
47+
: isSecond
48+
? 'bg-gradient-to-tr from-slate-300 via-slate-100 to-slate-400'
49+
: 'bg-gradient-to-tr from-orange-300 via-orange-100 to-orange-400'
5450
)}
5551
>
56-
{user.avatar ? (
52+
<div className="relative w-16 h-16 md:w-24 md:h-24 rounded-full overflow-hidden border-4 border-white dark:border-slate-900 bg-slate-200">
5753
<Image
5854
src={user.avatar}
5955
alt={`${user.username}'s avatar`}
60-
width={64}
61-
height={64}
62-
className="w-full h-full object-cover"
56+
fill
57+
className="object-cover"
6358
/>
64-
) : (
65-
<span className="text-2xl font-bold text-slate-700 dark:text-slate-200">
66-
{user.username.slice(0, 2).toUpperCase()}
67-
</span>
68-
)}
59+
</div>
6960
</div>
7061

7162
<div
7263
className={cn(
73-
'absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold border-2 border-white dark:border-slate-900 shadow-md',
74-
isFirst ? 'bg-yellow-500 text-white' : '',
75-
isSecond ? 'bg-slate-400 text-white' : '',
76-
isThird ? 'bg-orange-400 text-white' : ''
64+
'absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center justify-center w-7 h-7 md:w-9 md:h-9 rounded-full font-bold text-xs md:text-sm border-[3px] border-white dark:border-slate-900 shadow-lg',
65+
isFirst
66+
? 'bg-yellow-500 text-white'
67+
: isSecond
68+
? 'bg-slate-400 text-white'
69+
: 'bg-orange-400 text-white'
7770
)}
78-
aria-label={`Rank ${user.rank}`}
7971
>
8072
{user.rank}
8173
</div>
8274
</div>
8375

84-
<div className="text-center mb-3">
85-
<h3 className="font-bold text-slate-800 dark:text-slate-100 text-base mb-1 truncate max-w-[120px]">
76+
<div className="text-center mb-3 md:mb-5">
77+
<div className="font-bold text-slate-800 dark:text-slate-100 text-xs md:text-lg mb-1 truncate max-w-[85px] md:max-w-[140px]">
8678
{user.username}
87-
</h3>
88-
<div className="font-mono font-bold text-xl text-slate-900 dark:text-white tracking-tight">
89-
{user.points}{' '}
90-
<span className="text-xs font-normal text-slate-500">
91-
pts
92-
</span>
79+
</div>
80+
<div className="inline-block px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
81+
<div className="font-mono font-bold text-xs md:text-sm text-slate-700 dark:text-slate-300">
82+
{user.points}{' '}
83+
<span className="text-slate-400">{t('pts')}</span>
84+
</div>
9385
</div>
9486
</div>
9587

9688
<div
9789
aria-hidden="true"
9890
className={cn(
99-
'w-24 md:w-36 rounded-t-2xl border-x border-t shadow-lg backdrop-blur-sm',
100-
'bg-gradient-to-b from-white/80 via-white/40 to-transparent dark:from-slate-800/80 dark:via-slate-800/40',
91+
'w-20 md:w-40 rounded-t-2xl backdrop-blur-xl transition-all duration-300',
92+
'border-t-2 border-x-2 border-white/60 dark:border-white/10',
93+
10194
isFirst
102-
? 'h-40 border-yellow-400/30 dark:border-yellow-500/30'
103-
: '',
104-
isSecond
105-
? 'h-24 border-slate-300/30 dark:border-slate-500/30'
106-
: '',
107-
isThird
108-
? 'h-16 border-orange-300/30 dark:border-orange-500/30'
109-
: ''
95+
? 'h-40 md:h-56 bg-gradient-to-b from-yellow-200/30 to-yellow-500/5 dark:from-yellow-400/20 dark:to-transparent shadow-[0_0_50px_-10px_rgba(234,179,8,0.4)]'
96+
: isSecond
97+
? 'h-28 md:h-40 bg-gradient-to-b from-slate-200/30 to-slate-500/5 dark:from-slate-400/20 dark:to-transparent shadow-[0_0_40px_-10px_rgba(148,163,184,0.3)]'
98+
: 'h-20 md:h-28 bg-gradient-to-b from-orange-200/30 to-orange-500/5 dark:from-orange-400/20 dark:to-transparent shadow-[0_0_40px_-10px_rgba(251,146,60,0.3)]'
11099
)}
111100
/>
112-
</article>
101+
</div>
113102
</li>
114103
);
115104
})}

frontend/components/leaderboard/LeaderboardTable.tsx

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,77 @@
11
'use client';
22

33
import { TrendingUp } from 'lucide-react';
4+
import { useTranslations } from 'next-intl';
45
import { User } from './types';
56

67
export function LeaderboardTable({ users }: { users: User[] }) {
8+
const t = useTranslations('leaderboard');
9+
710
return (
8-
<div className="w-full bg-white/70 dark:bg-slate-900/60 backdrop-blur-md rounded-2xl shadow-xl border border-slate-200/60 dark:border-slate-700/60 overflow-hidden">
11+
<div className="w-full bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/50 dark:border-slate-700/50 overflow-hidden">
912
<table className="w-full text-left border-collapse">
10-
<caption className="sr-only">
11-
Leaderboard ranking for other participants
12-
</caption>
13+
<caption className="sr-only">{t('tableCaption')}</caption>
1314

1415
<thead className="bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-700">
1516
<tr>
1617
<th
1718
scope="col"
18-
className="px-6 py-4 text-center text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider w-[16%]"
19+
className="px-6 py-5 text-center text-xs font-extrabold text-slate-400 dark:text-slate-500 uppercase tracking-widest w-[15%]"
1920
>
20-
Rank
21+
{t('rank')}
2122
</th>
2223
<th
2324
scope="col"
24-
className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider w-[58%]"
25+
className="px-6 py-5 text-xs font-extrabold text-slate-400 dark:text-slate-500 uppercase tracking-widest w-[60%]"
2526
>
26-
UserName
27+
{t('user')}
2728
</th>
2829
<th
2930
scope="col"
30-
className="px-6 py-4 text-right text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider w-[25%]"
31+
className="px-6 py-5 text-right text-xs font-extrabold text-slate-400 dark:text-slate-500 uppercase tracking-widest w-[25%]"
3132
>
32-
Points
33+
{t('score')}
3334
</th>
3435
</tr>
3536
</thead>
3637

37-
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
38+
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
3839
{users.map(user => (
3940
<tr
4041
key={user.id}
41-
className="hover:bg-slate-50/80 dark:hover:bg-slate-800/60 transition-colors group"
42+
className="group transition-colors hover:bg-white/80 dark:hover:bg-slate-800/80"
4243
>
43-
<td className="px-6 py-4 text-center font-mono font-semibold text-slate-400 dark:text-slate-500 group-hover:text-slate-900 dark:group-hover:text-slate-200">
44+
<td className="px-6 py-4 text-center font-bold text-slate-500 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">
4445
{user.rank}
4546
</td>
4647

4748
<td className="px-6 py-4">
4849
<div className="flex items-center gap-4">
4950
<div
50-
className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-sm font-bold text-slate-600 dark:text-slate-300 ring-2 ring-transparent group-hover:ring-slate-200 dark:group-hover:ring-slate-600 transition-all"
51+
className="w-10 h-10 rounded-full bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900 flex items-center justify-center text-sm font-bold text-slate-600 dark:text-slate-300 shadow-inner group-hover:scale-110 transition-transform duration-300"
5152
aria-hidden="true"
5253
>
53-
<span>{user.username.slice(0, 1).toUpperCase()}</span>
54+
{user.username.slice(0, 1).toUpperCase()}
5455
</div>
56+
5557
<div className="flex flex-col">
56-
<span className="font-bold text-slate-700 dark:text-slate-200 text-sm group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
58+
<span className="font-bold text-slate-700 dark:text-slate-200 text-sm group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
5759
{user.username}
5860
</span>
59-
<span className="flex items-center gap-1 text-[10px] text-emerald-600 dark:text-emerald-400 font-medium">
60-
<TrendingUp className="w-3 h-3" aria-hidden="true" />
61-
Rising
62-
</span>
61+
{user.change > 0 && (
62+
<span className="flex items-center gap-1 text-[10px] text-emerald-600 dark:text-emerald-400 font-bold uppercase tracking-wide opacity-0 group-hover:opacity-100 transition-opacity">
63+
<TrendingUp className="w-3 h-3" aria-hidden="true" />
64+
{t('rising')}
65+
</span>
66+
)}
6367
</div>
6468
</div>
6569
</td>
6670

67-
<td className="px-6 py-4 text-right font-mono font-bold text-slate-800 dark:text-slate-100">
68-
{user.points.toLocaleString()}
71+
<td className="px-6 py-4 text-right">
72+
<span className="font-mono font-bold text-slate-800 dark:text-slate-100 group-hover:scale-105 inline-block transition-transform">
73+
{user.points.toLocaleString()}
74+
</span>
6975
</td>
7076
</tr>
7177
))}

0 commit comments

Comments
 (0)