Skip to content

Commit d0e2fed

Browse files
Merge pull request #316 from distributed-nerd/feat/accessibility-audit-and-fixes
Feat/accessibility audit and fixes
2 parents fd94071 + ec55849 commit d0e2fed

10 files changed

Lines changed: 272 additions & 85 deletions

File tree

frontend/src/app/courses/page.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { Course, coursesAPI } from "@/lib/api";
44
import Link from "next/link";
5-
import { coursesAPI, Course } from "@/lib/api";
5+
import { useEffect, useState } from "react";
66

77
export default function CoursesPage() {
88
const [courses, setCourses] = useState<Course[]>([]);
@@ -83,18 +83,23 @@ export default function CoursesPage() {
8383
{/* Search Bar */}
8484
<div className="mb-12 relative z-10">
8585
<div className="relative max-w-xl">
86+
<label htmlFor="course-search" className="sr-only">Search courses</label>
8687
<input
87-
type="text"
88+
id="course-search"
89+
type="search"
8890
placeholder="SEARCH NODE DIRECTORY..."
8991
value={searchTerm}
9092
onChange={(e) => setSearchTerm(e.target.value)}
9193
className="w-full px-5 py-4 pl-14 rounded-xl border border-white/20 bg-black text-white font-mono focus:ring-1 focus:ring-red-500 focus:border-red-500 transition-colors uppercase tracking-wide placeholder-gray-600 shadow-[0_4px_20px_rgba(0,0,0,0.5)]"
94+
aria-label="Search courses by title or description"
9295
/>
9396
<svg
9497
className="absolute left-5 top-1/2 transform -translate-y-1/2 w-6 h-6 text-red-600"
9598
fill="none"
9699
stroke="currentColor"
97100
viewBox="0 0 24 24"
101+
aria-hidden="true"
102+
focusable="false"
98103
>
99104
<path
100105
strokeLinecap="round"
@@ -109,13 +114,15 @@ export default function CoursesPage() {
109114
{/* Courses Grid */}
110115
<div className="relative z-10">
111116
{filteredCourses.length === 0 ? (
112-
<div className="text-center py-20 border border-white/10 rounded-2xl bg-zinc-950/50 backdrop-blur-sm">
113-
<div className="w-20 h-20 bg-red-900/20 rounded-2xl flex items-center justify-center mx-auto mb-6 border border-red-500/20">
117+
<div className="text-center py-20 border border-white/10 rounded-2xl bg-zinc-950/50 backdrop-blur-sm" role="status">
118+
<div className="w-20 h-20 bg-red-900/20 rounded-2xl flex items-center justify-center mx-auto mb-6 border border-red-500/20" aria-hidden="true">
114119
<svg
115120
className="h-10 w-10 text-red-500"
116121
fill="none"
117122
stroke="currentColor"
118123
viewBox="0 0 24 24"
124+
aria-hidden="true"
125+
focusable="false"
119126
>
120127
<path
121128
strokeLinecap="round"
@@ -135,22 +142,29 @@ export default function CoursesPage() {
135142
</p>
136143
</div>
137144
) : (
138-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
145+
<div
146+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
147+
aria-live="polite"
148+
aria-label={`${filteredCourses.length} course${filteredCourses.length !== 1 ? "s" : ""} found`}
149+
>
139150
{filteredCourses.map((course) => (
140151
<Link
141152
key={course.id}
142153
href={`/courses/${course.id}`}
143154
className="group bg-zinc-950 border border-white/10 rounded-2xl p-8 hover:border-red-500/50 hover:shadow-[0_0_30px_rgba(220,38,38,0.15)] hover:-translate-y-1 transition-all duration-300 flex flex-col h-full relative overflow-hidden"
155+
aria-label={`${course.title} - ${course.credits} credits, taught by ${course.instructor}`}
144156
>
145-
<div className="absolute top-0 right-0 w-32 h-32 bg-red-600/5 rounded-bl-[100px] pointer-events-none group-hover:bg-red-600/10 transition-colors"></div>
157+
<div className="absolute top-0 right-0 w-32 h-32 bg-red-600/5 rounded-bl-[100px] pointer-events-none group-hover:bg-red-600/10 transition-colors" aria-hidden="true"></div>
146158

147159
<div className="flex items-start justify-between mb-8 relative z-10">
148-
<div className="w-14 h-14 bg-black border border-white/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:border-red-500/50 group-hover:bg-red-500/10 transition-colors">
160+
<div className="w-14 h-14 bg-black border border-white/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:border-red-500/50 group-hover:bg-red-500/10 transition-colors" aria-hidden="true">
149161
<svg
150162
className="w-7 h-7 text-white group-hover:text-red-500 transition-colors"
151163
fill="none"
152164
stroke="currentColor"
153165
viewBox="0 0 24 24"
166+
aria-hidden="true"
167+
focusable="false"
154168
>
155169
<path
156170
strokeLinecap="round"
@@ -176,7 +190,7 @@ export default function CoursesPage() {
176190
<span className="text-xs font-bold text-gray-500 uppercase tracking-widest">
177191
{course.instructor}
178192
</span>
179-
<span className="inline-flex items-center gap-2 text-red-500 font-bold uppercase text-xs tracking-widest group-hover:text-red-400 transition-colors">
193+
<span className="inline-flex items-center gap-2 text-red-500 font-bold uppercase text-xs tracking-widest group-hover:text-red-400 transition-colors" aria-hidden="true">
180194
Enter Node{" "}
181195
<span className="transform group-hover:translate-x-1 transition-transform">
182196

frontend/src/app/globals.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,57 @@ html {
3030
scroll-behavior: smooth;
3131
}
3232

33+
/* Skip to main content link for keyboard/screen reader users */
34+
.skip-to-content {
35+
position: absolute;
36+
top: -100%;
37+
left: 0;
38+
z-index: 9999;
39+
padding: 0.75rem 1.5rem;
40+
background: #dc2626;
41+
color: #fff;
42+
font-weight: 700;
43+
font-size: 0.875rem;
44+
text-decoration: none;
45+
border-radius: 0 0 0.5rem 0;
46+
transition: top 0.2s;
47+
}
48+
49+
.skip-to-content:focus {
50+
top: 0;
51+
}
52+
53+
/* Screen reader only utility */
54+
.sr-only {
55+
position: absolute;
56+
width: 1px;
57+
height: 1px;
58+
padding: 0;
59+
margin: -1px;
60+
overflow: hidden;
61+
clip: rect(0, 0, 0, 0);
62+
white-space: nowrap;
63+
border-width: 0;
64+
}
65+
66+
/* Visible focus styles for keyboard navigation */
67+
:focus-visible {
68+
outline: 2px solid #dc2626;
69+
outline-offset: 2px;
70+
}
71+
72+
/* Respect reduced motion preferences */
73+
@media (prefers-reduced-motion: reduce) {
74+
*,
75+
*::before,
76+
*::after {
77+
animation-duration: 0.01ms !important;
78+
animation-iteration-count: 1 !important;
79+
transition-duration: 0.01ms !important;
80+
scroll-behavior: auto !important;
81+
}
82+
}
83+
3384
/* Custom scrollbar */
3485
::-webkit-scrollbar {
3586
width: 10px;

frontend/src/app/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { AuthProvider } from "@/contexts/AuthContext";
12
import type { Metadata } from "next";
23
import { Geist, Geist_Mono } from "next/font/google";
34
import "./globals.css";
4-
import { AuthProvider } from "@/contexts/AuthContext";
55

66
const geistSans = Geist({
77
variable: "--font-geist-sans",
@@ -32,8 +32,11 @@ export default function RootLayout({
3232
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-black text-white min-h-screen`}
3333
>
3434
<AuthProvider>
35+
<a href="#main-content" className="skip-to-content">
36+
Skip to main content
37+
</a>
3538
<Navbar />
36-
<main className="flex-grow">{children}</main>
39+
<main id="main-content" className="flex-grow">{children}</main>
3740
</AuthProvider>
3841
</body>
3942
</html>

0 commit comments

Comments
 (0)