made more blind friendly

This commit is contained in:
TheMaddax 2024-11-07 14:26:44 -06:00
parent b43b6b8e2a
commit b30579391a
4 changed files with 190 additions and 50 deletions

View file

@ -20,18 +20,30 @@ export const formatDateRange = (startDate: string, startTime: string, endDate: s
} else { } else {
return `${format(start, 'MMMM d, yyyy')}\n${formatTime(startDate, startTime)} - ${format(end, 'MMMM d, yyyy')}\n${formatTime(endDate, endTime)}`; return `${format(start, 'MMMM d, yyyy')}\n${formatTime(startDate, startTime)} - ${format(end, 'MMMM d, yyyy')}\n${formatTime(endDate, endTime)}`;
} }
};const EventPopup: React.FC<{ event: Event; position: { top: number; left: number }; onClose: () => void }> = ({ event, position, onClose }) => ( };
const EventPopup: React.FC<{ event: Event; position: { top: number; left: number }; onClose: () => void }> = ({ event, position, onClose }) => (
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4" className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4"
onClick={onClose} onClick={onClose}
role="dialog"
aria-modal="true"
aria-label={`Event Details: ${event.title}`}
> >
<motion.div <motion.div
className="bg-white p-4 rounded-lg shadow-lg max-w-xs w-full" className="bg-white p-4 rounded-lg shadow-lg max-w-xs w-full relative"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<button
onClick={onClose}
className="absolute top-2 right-2 p-2 text-gray-600 hover:text-gray-900"
aria-label="Close event details"
>
×
</button>
<h3 className="font-bold text-lg">{event.title}</h3> <h3 className="font-bold text-lg">{event.title}</h3>
<p className="text-sm whitespace-pre-line">{formatDateRange(event.date, event.startTime, event.date, event.endTime)}</p> <p className="text-sm whitespace-pre-line">{formatDateRange(event.date, event.startTime, event.date, event.endTime)}</p>
<p className="text-sm mt-2">{event.description}</p> <p className="text-sm mt-2">{event.description}</p>
@ -64,6 +76,19 @@ const Calendar: React.FC = () => {
}; };
}, [selectedEvent]); }, [selectedEvent]);
useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && selectedEvent) {
setSelectedEvent(null);
}
};
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('keydown', handleEscapeKey);
};
}, [selectedEvent]);
const isEventUpcoming = (eventDate: string) => { const isEventUpcoming = (eventDate: string) => {
const today = startOfDay(new Date()); const today = startOfDay(new Date());
const parsedEventDate = startOfDay(parseISO(eventDate)); const parsedEventDate = startOfDay(parseISO(eventDate));
@ -104,10 +129,11 @@ const Calendar: React.FC = () => {
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
className="text-2xl text-[#8C1D40]" className="text-2xl text-[#8C1D40]"
onClick={prevMonth} onClick={prevMonth}
aria-label={`Previous month: ${monthNames[(currentMonth.getMonth() - 1 + 12) % 12]}`}
> >
</motion.button> </motion.button>
<h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40]"> <h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40]" role="heading" aria-level={2}>
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()} {monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
</h2> </h2>
<motion.button <motion.button
@ -115,18 +141,33 @@ const Calendar: React.FC = () => {
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
className="text-2xl text-[#8C1D40]" className="text-2xl text-[#8C1D40]"
onClick={nextMonth} onClick={nextMonth}
aria-label={`Next month: ${monthNames[(currentMonth.getMonth() + 1) % 12]}`}
> >
</motion.button> </motion.button>
</div> </div>
<div className="grid grid-cols-7 gap-1 bg-gray-200 rounded-lg overflow-hidden"> <div
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => ( className="grid grid-cols-7 gap-1 bg-gray-200 rounded-lg overflow-hidden"
<div key={day} className="bg-[#8C1D40] text-white font-bold p-2 text-center text-xs sm:text-sm"> role="grid"
{day} aria-label="Calendar"
>
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(day => (
<div
key={day}
className="bg-[#8C1D40] text-white font-bold p-2 text-center text-xs sm:text-sm"
role="columnheader"
aria-label={day}
>
{day.charAt(0)}
</div> </div>
))} ))}
{emptyDays.map(day => ( {emptyDays.map(day => (
<div key={`empty-${day}`} className="bg-white p-2 h-16 sm:h-24"></div> <div
key={`empty-${day}`}
className="bg-white p-2 h-16 sm:h-24"
role="gridcell"
aria-hidden="true"
></div>
))} ))}
{days.map(day => { {days.map(day => {
const currentDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day); const currentDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
@ -138,19 +179,22 @@ const Calendar: React.FC = () => {
key={day} key={day}
className="bg-white p-1 sm:p-2 h-16 sm:h-24 cursor-pointer flex flex-col items-start justify-start overflow-hidden" className="bg-white p-1 sm:p-2 h-16 sm:h-24 cursor-pointer flex flex-col items-start justify-start overflow-hidden"
whileHover={{ backgroundColor: '#FFCCCB' }} whileHover={{ backgroundColor: '#FFCCCB' }}
role="gridcell"
aria-label={`${format(currentDate, 'MMMM d, yyyy')}${dayEvents.length ? `. ${dayEvents.length} events scheduled` : '. No events'}`}
> >
<span className="font-semibold text-xs sm:text-sm">{day}</span> <span className="font-semibold text-xs sm:text-sm">{day}</span>
{dayEvents.map((event, index) => ( {dayEvents.map((event, index) => (
<div <button
key={index} key={index}
className="text-xs bg-[#8C1D40] text-white p-1 mt-1 rounded w-full truncate" className="text-xs bg-[#8C1D40] text-white p-1 mt-1 rounded w-full truncate"
onClick={() => handleEventClick(event)} onClick={() => handleEventClick(event)}
aria-label={`${event.title} on ${format(parseISO(event.date), 'MMMM d')} from ${formatTime(event.date, event.startTime)} to ${formatTime(event.date, event.endTime)}`}
> >
<div className="truncate">{event.title}</div> <div className="truncate">{event.title}</div>
<div className="hidden sm:block"> <div className="hidden sm:block">
{formatTime(event.date, event.startTime)} - {formatTime(event.date, event.endTime)} {formatTime(event.date, event.startTime)} - {formatTime(event.date, event.endTime)}
</div> </div>
</div> </button>
))} ))}
</motion.div> </motion.div>
); );
@ -167,20 +211,27 @@ const Calendar: React.FC = () => {
</AnimatePresence> </AnimatePresence>
<div className="mt-12"> <div className="mt-12">
<h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40] mb-6">Upcoming Events</h2> <h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40] mb-6" role="heading" aria-level={2}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 justify-items-center"> Upcoming Events
{upcomingEvents.map((event, index) => ( </h2>
<EventCard <div
key={index} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 justify-items-center"
event={{ role="list"
...event, aria-label="List of upcoming events"
formattedDateRange: formatDateRange(event.date, event.startTime, event.date, event.endTime) >
}} {upcomingEvents.map((event, index) => (
/> <div key={index} role="listitem">
))} <EventCard
event={{
...event,
formattedDateRange: formatDateRange(event.date, event.startTime, event.date, event.endTime)
}}
/>
</div>
))}
</div>
</div> </div>
</div> </div>
</div>
); );
}; };

View file

@ -88,7 +88,7 @@ const Footer: React.FC = () => {
}; };
return ( return (
<footer className="bg-[#8C1D40] text-white py-8"> <footer className="bg-[#8C1D40] text-white py-8" role="contentinfo">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex flex-wrap justify-between"> <div className="flex flex-wrap justify-between">
<div className="w-full md:w-1/3 mb-6 md:mb-0"> <div className="w-full md:w-1/3 mb-6 md:mb-0">
@ -100,10 +100,11 @@ const Footer: React.FC = () => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-block mt-4 transition-transform duration-300 ease-in-out transform hover:scale-110" className="inline-block mt-4 transition-transform duration-300 ease-in-out transform hover:scale-110"
aria-label="Visit MCDi Facebook Page"
> >
<img <img
src="/logos/facebook-logo.png" src="/logos/facebook-logo.png"
alt="Facebook Logo" alt="MCDi Facebook Page"
className="w-10 h-10 rounded-full shadow-lg hover:shadow-xl" className="w-10 h-10 rounded-full shadow-lg hover:shadow-xl"
style={{ style={{
filter: 'drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.1))', filter: 'drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.1))',
@ -111,7 +112,7 @@ const Footer: React.FC = () => {
/> />
</a> </a>
</div> </div>
<div className="w-full md:w-1/3 mb-6 md:mb-0"> <nav className="w-full md:w-1/3 mb-6 md:mb-0" aria-label="Footer Navigation">
<h3 className="text-xl font-bold mb-4">Quick Links</h3> <h3 className="text-xl font-bold mb-4">Quick Links</h3>
<ul className="text-sm grid grid-cols-2 md:grid-cols-1 gap-2"> <ul className="text-sm grid grid-cols-2 md:grid-cols-1 gap-2">
<li><Link to="/" className="hover:text-yellow-200 transition-colors duration-200">Home</Link></li> <li><Link to="/" className="hover:text-yellow-200 transition-colors duration-200">Home</Link></li>
@ -120,12 +121,18 @@ const Footer: React.FC = () => {
<li><Link to="/sponsors" className="hover:text-yellow-200 transition-colors duration-200">Sponsors</Link></li> <li><Link to="/sponsors" className="hover:text-yellow-200 transition-colors duration-200">Sponsors</Link></li>
<li><Link to="/meet-board" className="hover:text-yellow-200 transition-colors duration-200">Meet MCDi Board</Link></li> <li><Link to="/meet-board" className="hover:text-yellow-200 transition-colors duration-200">Meet MCDi Board</Link></li>
</ul> </ul>
</div> </nav>
<div className="w-full md:w-1/3"> <div className="w-full md:w-1/3">
<h3 className="text-xl font-bold mb-4">Contact Us</h3> <h3 className="text-xl font-bold mb-4" id="contact-form">Contact Us</h3>
<form className="text-sm" onSubmit={handleSubmit}> <form
className="text-sm"
onSubmit={handleSubmit}
aria-labelledby="contact-form"
>
<div className="mb-2"> <div className="mb-2">
<label className="sr-only" htmlFor="name">Name</label>
<input <input
id="name"
type="text" type="text"
name="name" name="name"
value={formData.name} value={formData.name}
@ -133,11 +140,16 @@ const Footer: React.FC = () => {
placeholder="Name" placeholder="Name"
className={`w-full p-2 text-black rounded ${errors.name ? 'border-red-500' : ''}`} className={`w-full p-2 text-black rounded ${errors.name ? 'border-red-500' : ''}`}
required required
aria-required="true"
aria-invalid={errors.name ? "true" : "false"}
aria-describedby={errors.name ? "name-error" : undefined}
/> />
{errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>} {errors.name && <p id="name-error" className="text-red-500 text-xs mt-1" role="alert">{errors.name}</p>}
</div> </div>
<div className="mb-2"> <div className="mb-2">
<label className="sr-only" htmlFor="email">Email</label>
<input <input
id="email"
type="email" type="email"
name="email" name="email"
value={formData.email} value={formData.email}
@ -145,11 +157,16 @@ const Footer: React.FC = () => {
placeholder="Your Email Address" placeholder="Your Email Address"
className={`w-full p-2 text-black rounded ${errors.email ? 'border-red-500' : ''}`} className={`w-full p-2 text-black rounded ${errors.email ? 'border-red-500' : ''}`}
required required
aria-required="true"
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={errors.email ? "email-error" : undefined}
/> />
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>} {errors.email && <p id="email-error" className="text-red-500 text-xs mt-1" role="alert">{errors.email}</p>}
</div> </div>
<div className="mb-2"> <div className="mb-2">
<label className="sr-only" htmlFor="subject">Subject</label>
<input <input
id="subject"
type="text" type="text"
name="subject" name="subject"
value={formData.subject} value={formData.subject}
@ -157,11 +174,16 @@ const Footer: React.FC = () => {
placeholder="Subject" placeholder="Subject"
className={`w-full p-2 text-black rounded ${errors.subject ? 'border-red-500' : ''}`} className={`w-full p-2 text-black rounded ${errors.subject ? 'border-red-500' : ''}`}
required required
aria-required="true"
aria-invalid={errors.subject ? "true" : "false"}
aria-describedby={errors.subject ? "subject-error" : undefined}
/> />
{errors.subject && <p className="text-red-500 text-xs mt-1">{errors.subject}</p>} {errors.subject && <p id="subject-error" className="text-red-500 text-xs mt-1" role="alert">{errors.subject}</p>}
</div> </div>
<div className="mb-2"> <div className="mb-2">
<label className="sr-only" htmlFor="message">Message</label>
<textarea <textarea
id="message"
name="message" name="message"
value={formData.message} value={formData.message}
onChange={handleChange} onChange={handleChange}
@ -169,8 +191,11 @@ const Footer: React.FC = () => {
rows={3} rows={3}
className={`w-full p-2 text-black rounded ${errors.message ? 'border-red-500' : ''}`} className={`w-full p-2 text-black rounded ${errors.message ? 'border-red-500' : ''}`}
required required
aria-required="true"
aria-invalid={errors.message ? "true" : "false"}
aria-describedby={errors.message ? "message-error" : undefined}
></textarea> ></textarea>
{errors.message && <p className="text-red-500 text-xs mt-1">{errors.message}</p>} {errors.message && <p id="message-error" className="text-red-500 text-xs mt-1" role="alert">{errors.message}</p>}
</div> </div>
<input <input
type="text" type="text"
@ -180,14 +205,16 @@ const Footer: React.FC = () => {
style={{display: 'none'}} style={{display: 'none'}}
tabIndex={-1} tabIndex={-1}
autoComplete="off" autoComplete="off"
aria-hidden="true"
/> />
<button <button
type="submit" type="submit"
className="bg-yellow-400 text-[#8C1D40] font-bold py-2 px-4 rounded hover:bg-yellow-300 transition-colors duration-200" className="bg-yellow-400 text-[#8C1D40] font-bold py-2 px-4 rounded hover:bg-yellow-300 transition-colors duration-200"
aria-label="Send message"
> >
Send Send
</button> </button>
{status && <p className="mt-2 text-sm">{status}</p>} {status && <p className="mt-2 text-sm" role="status" aria-live="polite">{status}</p>}
</form> </form>
</div> </div>
</div> </div>
@ -199,6 +226,7 @@ const Footer: React.FC = () => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-yellow-200 hover:text-yellow-400 transition-colors duration-200" className="text-yellow-200 hover:text-yellow-400 transition-colors duration-200"
aria-label="Visit DeafGain LLC website (opens in new tab)"
> >
DeafGain, LLC DeafGain, LLC
</a> </a>

View file

@ -20,14 +20,14 @@ const Header: React.FC = () => {
}; };
return ( return (
<header className="bg-[#8C1D40] text-white shadow-md"> <header className="bg-[#8C1D40] text-white shadow-md" role="banner">
<div className="container mx-auto px-4 py-4"> <div className="container mx-auto px-4 py-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center"> <div className="flex items-center">
<img src="/mcdi-logo-small.png" alt="MCDi Logo" className="w-12 h-12 md:w-20 md:h-20 mr-2 md:mr-4" /> <img src="/mcdi-logo-small.png" alt="MCDi Logo" className="w-12 h-12 md:w-20 md:h-20 mr-2 md:mr-4" />
<h1 className="text-lg md:text-2xl font-bold">Missoula Council of the Deaf, Inc</h1> <h1 className="text-lg md:text-2xl font-bold">Missoula Council of the Deaf, Inc</h1>
</div> </div>
<nav className="hidden md:block"> <nav className="hidden md:block" role="navigation" aria-label="Main navigation">
<ul className="flex space-x-6 items-center"> <ul className="flex space-x-6 items-center">
<li><NavItem href="/">Home</NavItem></li> <li><NavItem href="/">Home</NavItem></li>
<li><AboutDropdown /></li> <li><AboutDropdown /></li>
@ -39,6 +39,9 @@ const Header: React.FC = () => {
<button <button
className="md:hidden text-white" className="md:hidden text-white"
onClick={toggleMenu} onClick={toggleMenu}
aria-label="Toggle navigation menu"
aria-expanded={isMenuOpen}
aria-controls="mobile-menu"
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16m-7 6h7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16m-7 6h7" />
@ -49,6 +52,9 @@ const Header: React.FC = () => {
<AnimatePresence> <AnimatePresence>
{isMenuOpen && ( {isMenuOpen && (
<motion.div <motion.div
id="mobile-menu"
role="navigation"
aria-label="Mobile navigation"
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }} animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }} exit={{ opacity: 0, height: 0 }}
@ -70,11 +76,16 @@ const Header: React.FC = () => {
contentLabel="Donate to MCDi" contentLabel="Donate to MCDi"
className="modal" className="modal"
overlayClassName="overlay" overlayClassName="overlay"
aria={{
labelledby: "donate-modal-title",
describedby: "donate-modal-description"
}}
> >
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<h2 className="text-2xl font-bold text-[#8C1D40] mb-4">Donate to MCDi</h2> <h2 id="donate-modal-title" className="text-2xl font-bold text-[#8C1D40] mb-4">Donate to MCDi</h2>
<div className="flex-grow overflow-y-auto"> <div id="donate-modal-description" className="flex-grow overflow-y-auto">
<iframe <iframe
title="Donation form"
src="https://www.zeffy.com/embed/donation-form/1cbff18e-c057-4fa6-8276-a993037de0bb" src="https://www.zeffy.com/embed/donation-form/1cbff18e-c057-4fa6-8276-a993037de0bb"
width="100%" width="100%"
height="100%" height="100%"
@ -84,6 +95,7 @@ const Header: React.FC = () => {
<button <button
onClick={closeDonateModal} onClick={closeDonateModal}
className="mt-4 bg-[#8C1D40] text-white font-bold py-2 px-4 rounded" className="mt-4 bg-[#8C1D40] text-white font-bold py-2 px-4 rounded"
aria-label="Close donation modal"
> >
Close Close
</button> </button>
@ -115,10 +127,14 @@ const AboutDropdown: React.FC = () => {
className="relative" className="relative"
onMouseEnter={() => setIsOpen(true)} onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)} onMouseLeave={() => setIsOpen(false)}
role="navigation"
aria-label="About MCDi dropdown"
> >
<Link <Link
to="/about" to="/about"
className="hover:text-yellow-200 transition-colors duration-200" className="hover:text-yellow-200 transition-colors duration-200"
aria-expanded={isOpen}
aria-controls="about-dropdown"
> >
<motion.span whileHover={{ scale: 1.05 }} className="inline-block"> <motion.span whileHover={{ scale: 1.05 }} className="inline-block">
About MCDi About MCDi
@ -127,12 +143,13 @@ const AboutDropdown: React.FC = () => {
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
<motion.div <motion.div
id="about-dropdown"
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -10 }}
className="absolute left-0 mt-2 w-48 bg-[#8C1D40] rounded-md shadow-lg z-10" className="absolute left-0 mt-2 w-48 bg-[#8C1D40] rounded-md shadow-lg z-10"
> >
<div className="py-1"> <div className="py-1" role="menu">
<DropdownItem href="/meet-board">Meet MCDi Board</DropdownItem> <DropdownItem href="/meet-board">Meet MCDi Board</DropdownItem>
<DropdownItem href="/bylaws">Bylaws</DropdownItem> <DropdownItem href="/bylaws">Bylaws</DropdownItem>
<DropdownItem href="/minutes">Minutes</DropdownItem> <DropdownItem href="/minutes">Minutes</DropdownItem>
@ -146,7 +163,11 @@ const AboutDropdown: React.FC = () => {
}; };
const DropdownItem: React.FC<NavItemProps> = ({ href, children }) => ( const DropdownItem: React.FC<NavItemProps> = ({ href, children }) => (
<Link to={href} className="block px-4 py-2 hover:bg-[#6B1631] transition-colors duration-200"> <Link
to={href}
className="block px-4 py-2 hover:bg-[#6B1631] transition-colors duration-200"
role="menuitem"
>
<motion.span whileHover={{ x: 5 }} className="inline-block"> <motion.span whileHover={{ x: 5 }} className="inline-block">
{children} {children}
</motion.span> </motion.span>
@ -161,6 +182,7 @@ const DonateButton: React.FC<{ onClick: (e: React.MouseEvent) => void }> = ({ on
<button <button
onClick={onClick} onClick={onClick}
className="inline-block bg-yellow-400 text-[#8C1D40] font-bold py-2 px-4 rounded-full hover:bg-yellow-300 transition-colors duration-200 shadow-md" className="inline-block bg-yellow-400 text-[#8C1D40] font-bold py-2 px-4 rounded-full hover:bg-yellow-300 transition-colors duration-200 shadow-md"
aria-label="Open donation form"
> >
Donate Donate
</button> </button>

View file

@ -124,9 +124,14 @@ const MeetBoard: React.FC = () => {
> >
<h1 className="text-3xl md:text-4xl font-bold text-[#8C1D40] mb-8">Meet MCDi Board</h1> <h1 className="text-3xl md:text-4xl font-bold text-[#8C1D40] mb-8">Meet MCDi Board</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8" role="list">
{shuffledMembers.map((member, index) => ( {shuffledMembers.map((member, index) => (
<BoardMemberCard key={index} member={member} onClick={() => setSelectedMember(member)} /> <BoardMemberCard
key={index}
member={member}
onClick={() => setSelectedMember(member)}
aria-label={`View details for ${member.name}, ${member.position}`}
/>
))} ))}
</div> </div>
@ -141,6 +146,7 @@ const MeetBoard: React.FC = () => {
className="w-full" className="w-full"
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
onEnded={() => setIsPlaying(false)} onEnded={() => setIsPlaying(false)}
aria-label="MCDi Introduction Video"
> >
<source src="/videos/mcdi-intro.mp4" type="video/mp4" /> <source src="/videos/mcdi-intro.mp4" type="video/mp4" />
Your browser does not support the video tag. Your browser does not support the video tag.
@ -152,21 +158,35 @@ const MeetBoard: React.FC = () => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 p-2 md:p-4" className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 p-2 md:p-4"
role="group"
aria-label="Video controls"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<button onClick={handleStop} className="text-white mr-2"> <button
onClick={handleStop}
className="text-white mr-2"
aria-label="Stop video"
>
<FaStop /> <FaStop />
</button> </button>
<button onClick={togglePlay} className="text-white mr-2"> <button
onClick={togglePlay}
className="text-white mr-2"
aria-label={isPlaying ? "Pause video" : "Play video"}
>
{isPlaying ? <FaPause /> : <FaPlay />} {isPlaying ? <FaPause /> : <FaPlay />}
</button> </button>
<input <input
type="range" type="range"
min="0" min={0}
max="100" max={100}
value={progress} value={progress}
onChange={handleSeek} onChange={handleSeek}
className="w-full mx-2" className="w-full mx-2"
aria-label="Video progress"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progress}
/> />
</div> </div>
</motion.div> </motion.div>
@ -184,6 +204,9 @@ const MeetBoard: React.FC = () => {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setSelectedMember(null)} onClick={() => setSelectedMember(null)}
role="dialog"
aria-modal="true"
aria-labelledby="member-modal-title"
> >
<motion.div <motion.div
initial={{ y: -50, opacity: 0 }} initial={{ y: -50, opacity: 0 }}
@ -192,10 +215,16 @@ const MeetBoard: React.FC = () => {
className="bg-white rounded-lg p-4 md:p-8 max-w-md w-full max-h-[90vh] overflow-y-auto" className="bg-white rounded-lg p-4 md:p-8 max-w-md w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<h2 className="text-xl md:text-2xl font-semibold text-[#8C1D40] mb-4">{selectedMember.name}</h2> <h2 id="member-modal-title" className="text-xl md:text-2xl font-semibold text-[#8C1D40] mb-4">{selectedMember.name}</h2>
<p className="text-base md:text-lg text-gray-700 mb-2">{selectedMember.position}</p> <p className="text-base md:text-lg text-gray-700 mb-2">{selectedMember.position}</p>
<p className="text-sm md:text-base text-gray-600 mb-4">{selectedMember.bio}</p> <p className="text-sm md:text-base text-gray-600 mb-4">{selectedMember.bio}</p>
<a href={`mailto:${selectedMember.email}`} className="text-blue-500 hover:underline text-sm md:text-base">{selectedMember.email}</a> <a
href={`mailto:${selectedMember.email}`}
className="text-blue-500 hover:underline text-sm md:text-base"
aria-label={`Send email to ${selectedMember.name}`}
>
{selectedMember.email}
</a>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}
@ -204,15 +233,25 @@ const MeetBoard: React.FC = () => {
); );
}; };
const BoardMemberCard: React.FC<{ member: BoardMember; onClick: () => void }> = ({ member, onClick }) => ( const BoardMemberCard: React.FC<{
member: BoardMember;
onClick: () => void;
'aria-label'?: string;
}> = ({ member, onClick, 'aria-label': ariaLabel }) => (
<motion.div <motion.div
className="bg-white rounded-lg shadow-md p-4 md:p-6 flex flex-col items-center cursor-pointer" className="bg-white rounded-lg shadow-md p-4 md:p-6 flex flex-col items-center cursor-pointer"
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
onClick={onClick} onClick={onClick}
role="listitem"
aria-label={ariaLabel}
> >
<h2 className="text-xl md:text-2xl font-semibold text-[#8C1D40] mb-2 md:mb-4">{member.name}</h2> <h2 className="text-xl md:text-2xl font-semibold text-[#8C1D40] mb-2 md:mb-4">{member.name}</h2>
<img src={member.image} alt={member.name} className="w-32 h-32 md:w-48 md:h-48 object-cover rounded-full mb-2 md:mb-4" /> <img
src={member.image}
alt={`${member.name}, ${member.position}`}
className="w-32 h-32 md:w-48 md:h-48 object-cover rounded-full mb-2 md:mb-4"
/>
<p className="text-base md:text-lg text-gray-700">{member.position}</p> <p className="text-base md:text-lg text-gray-700">{member.position}</p>
</motion.div> </motion.div>
); );