Skip to content

Commit 612db19

Browse files
committed
fix: resolve accessibility issues (W3C, Lighthouse 100%, Keyboard nav, VoiceOver)
1 parent 9be8fcc commit 612db19

7 files changed

Lines changed: 56 additions & 52 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>

frontend/components/leaderboard/LeaderboardClient.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { LeaderboardTabs } from './LeaderboardTabs';
55
import { LeaderboardPodium } from './LeaderboardPodium';
66
import { LeaderboardTable } from './LeaderboardTable';
77
import { User } from './types';
8-
import { Trophy } from 'lucide-react';
98

109
interface LeaderboardClientProps {
1110
initialUsers: User[];
@@ -32,20 +31,24 @@ export default function LeaderboardClient({
3231
className="pointer-events-none absolute inset-0 opacity-70 -z-10"
3332
aria-hidden="true"
3433
>
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" />
34+
<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" />
35+
<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" />
36+
<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" />
3837
</div>
3938

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">
39+
<div className="relative max-w-4xl mx-auto px-4 py-12 flex flex-col items-center z-10">
40+
<header className="text-center mb-12 animate-in fade-in slide-in-from-top-4 duration-700">
41+
<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">
42+
<span className="text-2xl mr-2" role="img" aria-label="Trophy">
43+
🏆
44+
</span>
45+
<span className="text-sm font-semibold text-slate-600 dark:text-slate-300 uppercase tracking-wider">
46+
Champions Arena
47+
</span>
48+
</div>
49+
<h1 className="text-4xl md:text-6xl font-extrabold mb-4 tracking-tight text-slate-900 dark:text-white drop-shadow-sm">
50+
Community{' '}
51+
<span className="bg-gradient-to-r from-sky-500 via-indigo-500 to-pink-500 bg-clip-text text-transparent">
4952
Leaderboard
5053
</span>
5154
</h1>
@@ -54,13 +57,14 @@ export default function LeaderboardClient({
5457
</p>
5558
</header>
5659

57-
<section className="mb-10 w-full flex justify-center">
60+
<div className="mb-10 w-full flex justify-center">
5861
<LeaderboardTabs activeTab={activeTab} onTabChange={setActiveTab} />
59-
</section>
62+
</div>
6063

6164
<section
6265
className="w-full flex flex-col items-center"
6366
role="tabpanel"
67+
id="leaderboard-panel"
6468
aria-label={`Results for ${activeTab}`}
6569
>
6670
<div className="w-full mb-12">
@@ -81,11 +85,11 @@ export default function LeaderboardClient({
8185
)}
8286
</div>
8387

84-
<div className="w-full animate-in fade-in slide-in-from-bottom-8 duration-700">
85-
<LeaderboardTable users={otherUsers} />
88+
<div className="w-full animate-in fade-in slide-in-from-bottom-8 duration-700 delay-200">
89+
{hasResults && <LeaderboardTable users={otherUsers} />}
8690
</div>
8791
</section>
88-
</main>
92+
</div>
8993
</div>
9094
);
9195
}

frontend/components/leaderboard/LeaderboardPodium.tsx

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,69 +22,61 @@ export function LeaderboardPodium({ topThree }: { topThree: User[] }) {
2222
key={user.id}
2323
className={cn(
2424
'flex flex-col items-center transition-all duration-500 relative',
25-
2625
isFirst ? 'order-2 z-10 -mt-8' : 'z-0',
2726
isSecond ? 'order-1' : '',
2827
isThird ? 'order-3' : ''
2928
)}
3029
>
31-
<article className="flex flex-col items-center">
30+
<div className="flex flex-col items-center">
3231
<div className="relative mb-4 group cursor-pointer">
3332
{isFirst && (
3433
<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}
34+
className="absolute -top-12 left-1/2 -translate-x-1/2 w-10 h-10 text-yellow-400 animate-bounce drop-shadow-lg"
3735
aria-hidden="true"
3836
/>
3937
)}
40-
4138
<div
4239
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',
40+
'relative rounded-full p-1 transition-transform duration-300 group-hover:scale-105',
4441
isFirst
45-
? 'w-28 h-28 border-yellow-400 dark:border-yellow-500'
42+
? 'bg-gradient-to-tr from-yellow-300 via-yellow-100 to-yellow-500 shadow-yellow-500/50 shadow-lg'
4643
: '',
4744
isSecond
48-
? 'w-20 h-20 border-slate-300 dark:border-slate-500'
45+
? 'bg-gradient-to-tr from-slate-300 via-slate-100 to-slate-400 shadow-slate-400/50 shadow-md'
4946
: '',
5047
isThird
51-
? 'w-20 h-20 border-orange-300 dark:border-orange-400'
52-
: '',
53-
'bg-white dark:bg-slate-800'
48+
? 'bg-gradient-to-tr from-orange-300 via-orange-100 to-orange-400 shadow-orange-400/50 shadow-md'
49+
: ''
5450
)}
5551
>
56-
{user.avatar ? (
52+
<div className="relative w-16 h-16 md:w-20 md:h-20 rounded-full overflow-hidden border-2 border-white dark:border-slate-800 bg-slate-200 dark:bg-slate-800">
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-8 h-8 rounded-full font-bold border-2 border-white dark:border-slate-900 shadow-md',
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

8476
<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]">
77+
<div className="font-bold text-slate-800 dark:text-slate-100 text-base mb-1 truncate max-w-[120px]">
8678
{user.username}
87-
</h3>
79+
</div>
8880
<div className="font-mono font-bold text-xl text-slate-900 dark:text-white tracking-tight">
8981
{user.points}{' '}
9082
<span className="text-xs font-normal text-slate-500">
@@ -105,11 +97,11 @@ export function LeaderboardPodium({ topThree }: { topThree: User[] }) {
10597
? 'h-24 border-slate-300/30 dark:border-slate-500/30'
10698
: '',
10799
isThird
108-
? 'h-16 border-orange-300/30 dark:border-orange-500/30'
100+
? 'h-20 border-orange-300/30 dark:border-orange-500/30'
109101
: ''
110102
)}
111103
/>
112-
</article>
104+
</div>
113105
</li>
114106
);
115107
})}

frontend/components/leaderboard/LeaderboardTabs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ export function LeaderboardTabs({
2727
onClick={() => onTabChange(tab)}
2828
role="tab"
2929
aria-selected={isActive}
30-
aria-controls={`panel-${tab}`}
30+
aria-controls="leaderboard-panel"
31+
id={`tab-${tab.replace(/\s+/g, '-')}`}
3132
className={cn(
3233
'px-5 py-2 text-sm font-semibold rounded-full transition-all duration-300 whitespace-nowrap',
3334
isActive
3435
? 'bg-gradient-to-r from-sky-500 via-indigo-500 to-pink-500 text-white shadow-md'
35-
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-white/50 dark:hover:bg-slate-700/50'
36+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-white/50 dark:hover:bg-slate-700/50'
3637
)}
3738
>
3839
{tab}

frontend/components/shared/CookieBanner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ export function CookieBanner() {
4444
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6 animate-in slide-in-from-bottom-full fade-in duration-700">
4545
<div className="mx-auto max-w-4xl rounded-2xl border border-gray-200 bg-white/90 p-5 shadow-2xl backdrop-blur-md dark:border-gray-800 dark:bg-gray-900/90 md:flex md:items-center md:justify-between md:gap-6">
4646
<div className="mb-4 md:mb-0 md:flex-1">
47-
<h3 className="mb-1 text-lg font-semibold text-gray-900 dark:text-white">
47+
<div className="mb-1 text-lg font-semibold text-gray-900 dark:text-white">
4848
{t('title')}
49-
</h3>
49+
</div>
5050
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
5151
{t('description')}{' '}
5252
<Link

frontend/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)