made more blind friendly
This commit is contained in:
parent
b43b6b8e2a
commit
b30579391a
4 changed files with 190 additions and 50 deletions
|
|
@ -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,21 +211,28 @@ 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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Calendar;
|
export default Calendar;
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue