Add in support for mobile browsers
This commit is contained in:
parent
05ec2484af
commit
e47efca847
11 changed files with 301 additions and 186 deletions
|
|
@ -41,6 +41,8 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"@types/styled-components": "^5.1.29",
|
||||
"autoprefixer": "^10.4.16",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const App: React.FC = () => {
|
|||
<Router>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
<main className="flex-grow px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/calendar" element={<Calendar />} />
|
||||
|
|
|
|||
|
|
@ -4,25 +4,41 @@ import EventCard from './EventCard';
|
|||
import { parseISO, startOfDay, isSameDay, isAfter, format, startOfMonth } from 'date-fns';
|
||||
import { events, Event } from '../eventData';
|
||||
|
||||
const formatTime = (dateString: string, time: string): string => {
|
||||
export const formatTime = (dateString: string, time: string): string => {
|
||||
const date = parseISO(dateString);
|
||||
const [hours, minutes] = time.split(':');
|
||||
const eventDate = new Date(date.setHours(parseInt(hours, 10), parseInt(minutes, 10)));
|
||||
return format(eventDate, 'MMMM d, yyyy h:mm a');
|
||||
return format(eventDate, 'h:mm a');
|
||||
};
|
||||
|
||||
const EventPopup: React.FC<{ event: Event; position: { top: number; left: number } }> = ({ event, position }) => (
|
||||
export const formatDateRange = (startDate: string, startTime: string, endDate: string, endTime: string): string => {
|
||||
const start = parseISO(`${startDate}T${startTime}`);
|
||||
const end = parseISO(`${endDate}T${endTime}`);
|
||||
|
||||
if (isSameDay(start, end)) {
|
||||
return `${format(start, 'MMMM d, yyyy')} ${formatTime(startDate, startTime)} - ${formatTime(endDate, endTime)}`;
|
||||
} else {
|
||||
return `${format(start, 'MMMM d, yyyy h:mm a')} - ${format(end, 'MMMM d, yyyy h:mm a')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const EventPopup: React.FC<{ event: Event; position: { top: number; left: number }; onClose: () => void }> = ({ event, position, onClose }) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="absolute z-10 bg-white p-4 rounded-lg shadow-lg"
|
||||
style={{ top: position.top, left: position.left }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<h3 className="font-bold text-lg">{event.title}</h3>
|
||||
<p>{formatTime(event.date, event.startTime)} - {formatTime(event.date, event.endTime)}</p>
|
||||
<p>{event.description}</p>
|
||||
<p>Contact: <a href={`mailto:${event.email}`} className="text-blue-500 hover:underline">{event.pointOfContact}</a></p>
|
||||
<motion.div
|
||||
className="bg-white p-4 rounded-lg shadow-lg max-w-xs w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="font-bold text-lg">{event.title}</h3>
|
||||
<p className="text-sm">{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">Contact: <a href={`mailto:${event.email}`} className="text-blue-500 hover:underline">{event.pointOfContact}</a></p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
|
|
@ -32,10 +48,23 @@ const Calendar: React.FC = () => {
|
|||
return startOfMonth(now);
|
||||
});
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
||||
const [isPopupClosing, setIsPopupClosing] = useState(false);
|
||||
const calendarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
if (selectedEvent && calendarRef.current && !calendarRef.current.contains(event.target as Node)) {
|
||||
setSelectedEvent(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('touchstart', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('touchstart', handleClickOutside);
|
||||
};
|
||||
}, [selectedEvent]);
|
||||
|
||||
const isEventUpcoming = (eventDate: string) => {
|
||||
const today = startOfDay(new Date());
|
||||
const parsedEventDate = startOfDay(parseISO(eventDate));
|
||||
|
|
@ -60,53 +89,8 @@ const Calendar: React.FC = () => {
|
|||
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (calendarRef.current && !calendarRef.current.contains(event.target as Node)) {
|
||||
setSelectedEvent(null);
|
||||
} else if (selectedEvent) {
|
||||
setSelectedEvent(null);
|
||||
setIsPopupClosing(true);
|
||||
setTimeout(() => setIsPopupClosing(false), 0);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [selectedEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
const currentMonthEvents = events.filter(event => {
|
||||
const eventDate = parseISO(event.date);
|
||||
return eventDate.getMonth() === now.getMonth() && eventDate.getFullYear() === now.getFullYear();
|
||||
});
|
||||
|
||||
if (currentMonthEvents.length === 0) {
|
||||
// If no events in the current month, find the next month with events
|
||||
let nextMonth = now;
|
||||
while (true) {
|
||||
nextMonth = new Date(nextMonth.getFullYear(), nextMonth.getMonth() + 1, 1);
|
||||
const nextMonthEvents = events.filter(event => {
|
||||
const eventDate = parseISO(event.date);
|
||||
return eventDate.getMonth() === nextMonth.getMonth() && eventDate.getFullYear() === nextMonth.getFullYear();
|
||||
});
|
||||
if (nextMonthEvents.length > 0) {
|
||||
setCurrentMonth(startOfMonth(nextMonth));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEventClick = (event: Event, e: React.MouseEvent) => {
|
||||
if (!isPopupClosing) {
|
||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
||||
setPopupPosition({ top: rect.bottom, left: rect.left });
|
||||
setSelectedEvent(event);
|
||||
}
|
||||
const handleEventClick = (event: Event) => {
|
||||
setSelectedEvent(event);
|
||||
};
|
||||
|
||||
const upcomingEvents = events
|
||||
|
|
@ -114,7 +98,7 @@ const Calendar: React.FC = () => {
|
|||
.sort((a, b) => parseISO(a.date).getTime() - parseISO(b.date).getTime());
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 relative" ref={calendarRef}>
|
||||
<div className="max-w-4xl mx-auto p-4 sm:p-6 relative" ref={calendarRef}>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
|
|
@ -124,7 +108,7 @@ const Calendar: React.FC = () => {
|
|||
>
|
||||
←
|
||||
</motion.button>
|
||||
<h2 className="text-2xl font-bold text-[#8C1D40]">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40]">
|
||||
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
||||
</h2>
|
||||
<motion.button
|
||||
|
|
@ -137,13 +121,13 @@ const Calendar: React.FC = () => {
|
|||
</motion.button>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 bg-gray-200 rounded-lg overflow-hidden">
|
||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
||||
<div key={day} className="bg-[#8C1D40] text-white font-bold p-2 text-center">
|
||||
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => (
|
||||
<div key={day} className="bg-[#8C1D40] text-white font-bold p-2 text-center text-xs sm:text-sm">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
{emptyDays.map(day => (
|
||||
<div key={`empty-${day}`} className="bg-white p-2 h-24"></div>
|
||||
<div key={`empty-${day}`} className="bg-white p-2 h-16 sm:h-24"></div>
|
||||
))}
|
||||
{days.map(day => {
|
||||
const currentDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
|
||||
|
|
@ -153,18 +137,20 @@ const Calendar: React.FC = () => {
|
|||
return (
|
||||
<motion.div
|
||||
key={day}
|
||||
className="bg-white p-2 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' }}
|
||||
>
|
||||
<span className="font-semibold">{day}</span>
|
||||
<span className="font-semibold text-xs sm:text-sm">{day}</span>
|
||||
{dayEvents.map((event, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-xs bg-[#8C1D40] text-white p-1 mt-1 rounded w-full"
|
||||
onClick={(e) => handleEventClick(event, e)}
|
||||
className="text-xs bg-[#8C1D40] text-white p-1 mt-1 rounded w-full truncate"
|
||||
onClick={() => handleEventClick(event)}
|
||||
>
|
||||
<div>{event.title}</div>
|
||||
<div>{`${formatTime(event.date, event.startTime).split(' ')[3]} - ${formatTime(event.date, event.endTime).split(' ')[3]}`}</div>
|
||||
<div className="truncate">{event.title}</div>
|
||||
<div className="hidden sm:block">
|
||||
{formatTime(event.date, event.startTime)} - {formatTime(event.date, event.endTime)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
|
@ -175,27 +161,27 @@ const Calendar: React.FC = () => {
|
|||
{selectedEvent && (
|
||||
<EventPopup
|
||||
event={selectedEvent}
|
||||
position={popupPosition}
|
||||
position={{ top: 0, left: 0 }}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold text-[#8C1D40] mb-6">Upcoming Events</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 justify-items-center">
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard
|
||||
key={index}
|
||||
event={{
|
||||
...event,
|
||||
formattedStartTime: formatTime(event.date, event.startTime),
|
||||
formattedEndTime: formatTime(event.date, event.endTime)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-[#8C1D40] mb-6">Upcoming Events</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 justify-items-center">
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard
|
||||
key={index}
|
||||
event={{
|
||||
...event,
|
||||
formattedDateRange: formatDateRange(event.date, event.startTime, event.date, event.endTime)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,25 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
import { Event } from '../eventData';
|
||||
|
||||
interface EventCardProps {
|
||||
event: {
|
||||
date: string;
|
||||
title: string;
|
||||
description: string;
|
||||
formattedStartTime: string;
|
||||
formattedEndTime: string;
|
||||
pointOfContact: string;
|
||||
email: string;
|
||||
event: Event & {
|
||||
formattedDateRange: string;
|
||||
};
|
||||
}
|
||||
|
||||
const EventCard: React.FC<EventCardProps> = ({ event }) => {
|
||||
const startDate = new Date(event.formattedStartTime);
|
||||
const endDate = new Date(event.formattedEndTime);
|
||||
|
||||
const formattedDate = format(startDate, 'MMMM d, yyyy');
|
||||
const formattedTime = `${format(startDate, 'h:mm a')} - ${format(endDate, 'h:mm a')}`;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="bg-white rounded-lg shadow-md p-4 w-full max-w-xs"
|
||||
>
|
||||
<h3 className="font-bold text-lg text-[#8C1D40] mb-2">{event.title}</h3>
|
||||
<p className="text-sm text-gray-600 mb-1">{formattedDate}</p>
|
||||
<p className="text-sm mb-2">{formattedTime}</p>
|
||||
<p className="text-sm text-gray-600 mb-2">{event.formattedDateRange}</p>
|
||||
<p className="text-sm mb-2">{event.description}</p>
|
||||
<p className="text-sm">Contact: <a href={`mailto:${event.email}`} className="text-blue-500 hover:underline">{event.pointOfContact}</a></p>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventCard;
|
||||
export default EventCard;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const Footer: React.FC = () => {
|
|||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
honeypot: '', // Honeypot field
|
||||
honeypot: '',
|
||||
});
|
||||
const [status, setStatus] = useState('');
|
||||
const [errors, setErrors] = useState<Partial<typeof formData>>({});
|
||||
|
|
@ -45,15 +45,13 @@ const Footer: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check honeypot field
|
||||
if (formData.honeypot) {
|
||||
setStatus('Form submission rejected.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastSubmissionTime < 60000) { // 1 minute cooldown
|
||||
if (currentTime - lastSubmissionTime < 60000) {
|
||||
setStatus('Please wait a moment before submitting again.');
|
||||
return;
|
||||
}
|
||||
|
|
@ -115,12 +113,12 @@ const Footer: React.FC = () => {
|
|||
</div>
|
||||
<div className="w-full md:w-1/3 mb-6 md:mb-0">
|
||||
<h3 className="text-xl font-bold mb-4">Quick Links</h3>
|
||||
<ul className="text-sm">
|
||||
<li className="mb-2"><Link to="/" className="hover:text-yellow-200 transition-colors duration-200">Home</Link></li>
|
||||
<li className="mb-2"><Link to="/calendar" className="hover:text-yellow-200 transition-colors duration-200">Calendar</Link></li>
|
||||
<li className="mb-2"><Link to="/bylaws" className="hover:text-yellow-200 transition-colors duration-200">Bylaws</Link></li>
|
||||
<li className="mb-2"><Link to="/sponsors" className="hover:text-yellow-200 transition-colors duration-200">Sponsors</Link></li>
|
||||
<li className="mb-2"><Link to="/meet-board" className="hover:text-yellow-200 transition-colors duration-200">Meet MCDi Board</Link></li>
|
||||
<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="/calendar" className="hover:text-yellow-200 transition-colors duration-200">Calendar</Link></li>
|
||||
<li><Link to="/bylaws" className="hover:text-yellow-200 transition-colors duration-200">Bylaws</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>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full md:w-1/3">
|
||||
|
|
@ -198,4 +196,4 @@ const Footer: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,17 @@ import { Link } from 'react-router-dom';
|
|||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="bg-[#8C1D40] text-white shadow-md">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<img src="/mcdi-logo-small.png" alt="MCDi Logo" className="w-20 h-20 mr-4" />
|
||||
<h1 className="text-2xl font-bold">Missoula Council of the Deaf, Inc</h1>
|
||||
<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>
|
||||
</div>
|
||||
<nav>
|
||||
<nav className="hidden md:block">
|
||||
<ul className="flex space-x-6 items-center">
|
||||
<li><NavItem href="/">Home</NavItem></li>
|
||||
<li><AboutDropdown /></li>
|
||||
|
|
@ -20,8 +22,34 @@ const Header: React.FC = () => {
|
|||
<li><DonateButton /></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<button
|
||||
className="md:hidden text-white"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="md:hidden bg-[#8C1D40] px-4 py-2"
|
||||
>
|
||||
<ul className="space-y-2">
|
||||
<li><NavItem href="/" onClick={() => setIsMenuOpen(false)}>Home</NavItem></li>
|
||||
<li><NavItem href="/about" onClick={() => setIsMenuOpen(false)}>About MCDi</NavItem></li>
|
||||
<li><NavItem href="/calendar" onClick={() => setIsMenuOpen(false)}>Calendar</NavItem></li>
|
||||
<li><NavItem href="/joinmcdi" onClick={() => setIsMenuOpen(false)}>Join MCDi</NavItem></li>
|
||||
<li><DonateButton onClick={() => setIsMenuOpen(false)} /></li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
|
@ -29,15 +57,17 @@ const Header: React.FC = () => {
|
|||
interface NavItemProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const NavItem: React.FC<NavItemProps> = ({ href, children }) => (
|
||||
<Link to={href} className="hover:text-yellow-200 transition-colors duration-200">
|
||||
const NavItem: React.FC<NavItemProps> = ({ href, children, onClick }) => (
|
||||
<Link to={href} className="block hover:text-yellow-200 transition-colors duration-200" onClick={onClick}>
|
||||
<motion.span whileHover={{ scale: 1.05 }} className="inline-block">
|
||||
{children}
|
||||
</motion.span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const AboutDropdown: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
|
@ -75,6 +105,7 @@ const AboutDropdown: React.FC = () => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownItem: React.FC<NavItemProps> = ({ href, children }) => (
|
||||
<Link to={href} className="block px-4 py-2 hover:bg-[#6B1631] transition-colors duration-200">
|
||||
<motion.span whileHover={{ x: 5 }} className="inline-block">
|
||||
|
|
@ -83,15 +114,15 @@ const DropdownItem: React.FC<NavItemProps> = ({ href, children }) => (
|
|||
</Link>
|
||||
);
|
||||
|
||||
const DonateButton: React.FC = () => (
|
||||
const DonateButton: React.FC<{ onClick?: () => void }> = ({ onClick }) => (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Link to="/donate" 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">
|
||||
<Link to="/donate" 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">
|
||||
Donate
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,9 @@ import React, { useState, useEffect } from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import ImageCarousel from './ImageCarousel';
|
||||
import EventCard from './EventCard';
|
||||
import { parseISO, startOfDay, isSameDay, isAfter, format } from 'date-fns';
|
||||
import { parseISO, startOfDay, isSameDay, isAfter } from 'date-fns';
|
||||
import { events, Event } from '../eventData';
|
||||
|
||||
const formatTime = (dateString: string, time: string): string => {
|
||||
const date = parseISO(dateString);
|
||||
const [hours, minutes] = time.split(':');
|
||||
const eventDate = new Date(date.setHours(parseInt(hours, 10), parseInt(minutes, 10)));
|
||||
return format(eventDate, 'MMMM d, yyyy h:mm a');
|
||||
};
|
||||
import { formatDateRange } from './Calendar';
|
||||
|
||||
const isEventUpcoming = (eventDate: string) => {
|
||||
const today = startOfDay(new Date());
|
||||
|
|
@ -35,13 +29,14 @@ const Home: React.FC = () => {
|
|||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-[#8C1D40] mb-8">Welcome to MCDi</h1>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-[#8C1D40] mb-8 text-center">Welcome to MCDi</h1>
|
||||
|
||||
<ImageCarousel />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
<div className="grid grid-rows-2 gap-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="space-y-8">
|
||||
<InfoCard
|
||||
title="Empowering Deaf Culture and Community"
|
||||
content="We're proud to be the Missoula Council of the Deaf, Inc. (MCDi) in the heart of Missoula, Montana. Our organization is dedicated to promoting Deaf culture, community, and systematic change in our beautiful state."
|
||||
|
|
@ -59,15 +54,14 @@ const Home: React.FC = () => {
|
|||
key={index}
|
||||
event={{
|
||||
...event,
|
||||
formattedStartTime: formatTime(event.date, event.startTime),
|
||||
formattedEndTime: formatTime(event.date, event.endTime)
|
||||
formattedDateRange: formatDateRange(event.date, event.startTime, event.date, event.endTime)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<InfoCard
|
||||
title="Join Us: Support Change in Montana"
|
||||
content="As a parent or ally, you can make a difference by joining MCDi. Together, we'll work toward creating a more inclusive environment for all Montanans, regardless of communication style. Your support will help shape the future of Deaf education, employment, and accessibility in our great state."
|
||||
|
|
@ -92,8 +86,8 @@ const InfoCard: React.FC<{ title: string; content: string }> = ({ title, content
|
|||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-[#8C1D40] mb-4">{title}</h2>
|
||||
<p className="text-gray-700 flex-grow">{content}</p>
|
||||
<h2 className="text-xl md:text-2xl font-semibold text-[#8C1D40] mb-4">{title}</h2>
|
||||
<p className="text-gray-700 flex-grow text-sm md:text-base">{content}</p>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -122,18 +122,13 @@ const MeetBoard: React.FC = () => {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h1 className="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 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{shuffledMembers.slice(0, 3).map((member, index) => (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8">
|
||||
{shuffledMembers.map((member, index) => (
|
||||
<BoardMemberCard key={index} member={member} onClick={() => setSelectedMember(member)} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex justify-center gap-8">
|
||||
{shuffledMembers.slice(3, 5).map((member, index) => (
|
||||
<BoardMemberCard key={index + 3} member={member} onClick={() => setSelectedMember(member)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<div
|
||||
|
|
@ -156,7 +151,7 @@ const MeetBoard: React.FC = () => {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 p-4"
|
||||
className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 p-2 md:p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<button onClick={handleStop} className="text-white mr-2">
|
||||
|
|
@ -187,20 +182,20 @@ const MeetBoard: React.FC = () => {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={() => setSelectedMember(null)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: -50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -50, opacity: 0 }}
|
||||
className="bg-white rounded-lg p-8 max-w-md"
|
||||
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()}
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-[#8C1D40] mb-4">{selectedMember.name}</h2>
|
||||
<p className="text-lg text-gray-700 mb-2">{selectedMember.position}</p>
|
||||
<p className="text-gray-600 mb-4">{selectedMember.bio}</p>
|
||||
<a href={`mailto:${selectedMember.email}`} className="text-blue-500 hover:underline">{selectedMember.email}</a>
|
||||
<h2 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-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>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
@ -211,14 +206,14 @@ const MeetBoard: React.FC = () => {
|
|||
|
||||
const BoardMemberCard: React.FC<{ member: BoardMember; onClick: () => void }> = ({ member, onClick }) => (
|
||||
<motion.div
|
||||
className="bg-white rounded-lg shadow-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 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-[#8C1D40] mb-4">{member.name}</h2>
|
||||
<img src={member.image} alt={member.name} className="w-48 h-48 object-cover rounded-full mb-4" />
|
||||
<p className="text-lg text-gray-700">{member.position}</p>
|
||||
<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" />
|
||||
<p className="text-base md:text-lg text-gray-700">{member.position}</p>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ interface SponsorCardProps {
|
|||
|
||||
const SponsorCard: React.FC<SponsorCardProps> = ({ name, logoPath, onClick }) => (
|
||||
<motion.div
|
||||
className="bg-white rounded-lg shadow-md p-6 flex flex-col items-center h-full cursor-pointer"
|
||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6 flex flex-col items-center h-full cursor-pointer"
|
||||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-[#8C1D40] mb-4">{name}</h2>
|
||||
<div className="w-full h-48 flex items-center justify-center">
|
||||
<h2 className="text-xl sm:text-2xl font-semibold text-[#8C1D40] mb-2 sm:mb-4 text-center">{name}</h2>
|
||||
<div className="w-full h-32 sm:h-48 flex items-center justify-center">
|
||||
<img src={logoPath} alt={`${name} logo`} className="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -26,21 +26,21 @@ const Popup: React.FC<{ isOpen: boolean; onClose: () => void; children: React.Re
|
|||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-white rounded-lg p-6 max-w-md"
|
||||
className="bg-white rounded-lg p-4 sm:p-6 w-full max-w-md"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
<button className="mt-4 bg-[#8C1D40] text-white px-4 py-2 rounded" onClick={onClose}>Close</button>
|
||||
<button className="mt-4 bg-[#8C1D40] text-white px-4 py-2 rounded w-full sm:w-auto" onClick={onClose}>Close</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
@ -63,9 +63,9 @@ const Sponsors: React.FC = () => {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-[#8C1D40] mb-8">Our Sponsors</h1>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-[#8C1D40] mb-6 sm:mb-8 text-center">Our Sponsors</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-8">
|
||||
<SponsorCard
|
||||
name="Drum Coffee"
|
||||
logoPath="/sponsors/drum-coffee-missoula.png"
|
||||
|
|
@ -78,24 +78,20 @@ const Sponsors: React.FC = () => {
|
|||
description={sponsorInfo["Imagine Nation Brewing"]}
|
||||
onClick={() => setSelectedSponsor("Imagine Nation Brewing")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 flex justify-center">
|
||||
<div className="w-full md:w-1/2">
|
||||
<SponsorCard
|
||||
name="Gild Brewing"
|
||||
logoPath="/sponsors/gild-brewing.png"
|
||||
description={sponsorInfo["Gild Brewing"]}
|
||||
onClick={() => setSelectedSponsor("Gild Brewing")}
|
||||
/>
|
||||
</div>
|
||||
<SponsorCard
|
||||
name="Gild Brewing"
|
||||
logoPath="/sponsors/gild-brewing.png"
|
||||
description={sponsorInfo["Gild Brewing"]}
|
||||
onClick={() => setSelectedSponsor("Gild Brewing")}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<Popup isOpen={!!selectedSponsor} onClose={() => setSelectedSponsor(null)}>
|
||||
{selectedSponsor && (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold mb-4">{selectedSponsor}</h2>
|
||||
<p>{sponsorInfo[selectedSponsor as keyof typeof sponsorInfo]}</p>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">{selectedSponsor}</h2>
|
||||
<p className="text-sm sm:text-base">{sponsorInfo[selectedSponsor as keyof typeof sponsorInfo]}</p>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
|
|
@ -103,4 +99,4 @@ const Sponsors: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Sponsors;
|
||||
export default Sponsors;
|
||||
|
|
@ -2,4 +2,108 @@
|
|||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
/* You can add any custom styles here */
|
||||
/* Custom styles */
|
||||
html, body {
|
||||
@apply antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Mobile-first approach */
|
||||
.container {
|
||||
@apply px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
/* Header styles */
|
||||
.header-nav {
|
||||
@apply flex flex-col sm:flex-row items-center justify-between;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply block py-2 px-4 text-center sm:inline-block sm:py-0;
|
||||
}
|
||||
|
||||
/* Footer styles */
|
||||
.footer-content {
|
||||
@apply flex flex-col sm:flex-row justify-between items-start sm:items-center;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
@apply w-full sm:w-1/3 mb-6 sm:mb-0;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-input {
|
||||
@apply w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-[#8C1D40] focus:border-transparent;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-md transition duration-300 ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-[#8C1D40] text-white hover:bg-[#6B1631];
|
||||
}
|
||||
|
||||
/* Responsive typography */
|
||||
h1 {
|
||||
@apply text-3xl sm:text-4xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl sm:text-3xl font-semibold;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for webkit browsers */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-[#8C1D40] rounded-full;
|
||||
}
|
||||
|
||||
/* Smooth scrolling for the entire page */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Improved focus styles for accessibility */
|
||||
*:focus {
|
||||
@apply outline-none ring-2 ring-[#8C1D40] ring-opacity-50;
|
||||
}
|
||||
|
||||
/* Custom utility classes */
|
||||
.text-shadow {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hover-grow {
|
||||
@apply transition-transform duration-300 ease-in-out;
|
||||
}
|
||||
|
||||
.hover-grow:hover {
|
||||
@apply transform scale-105;
|
||||
}
|
||||
|
||||
/* Responsive grid layouts */
|
||||
.grid-responsive {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6;
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,32 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./public/index.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./public/index.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'mcdi-maroon': '#8C1D40',
|
||||
'mcdi-gold': '#FFC627',
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Roboto', 'Arial', 'sans-serif'],
|
||||
'serif': ['Merriweather', 'Georgia', 'serif'],
|
||||
},
|
||||
screens: {
|
||||
'xs': '475px',
|
||||
},
|
||||
spacing: {
|
||||
'128': '32rem',
|
||||
'144': '36rem',
|
||||
},
|
||||
minHeight: {
|
||||
'1/2': '50vh',
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue