diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx index 5461abca13..e7201a98f9 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx @@ -2,176 +2,94 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import ReactCalendar from 'react-calendar'; import 'react-calendar/dist/Calendar.css'; +import axios from 'axios'; +import { ENDPOINTS } from '../../../utils/URL'; import CalendarActivitySection from './CalendarActivitySection'; import styles from './CommunityCalendar.module.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faClock, faLocationDot, faTag, faCircleCheck } from '@fortawesome/free-solid-svg-icons'; -const mockEvents = [ - { - id: 1, - title: 'Event 1', - type: 'Workshop', - location: 'Virtual', - time: '10:00 AM', - date: new Date(2025, 0, 27), - status: 'New', - description: 'Detailed description of Event 1.', - registeredCount: 1, - capacity: 20, - }, - { - id: 2, - title: 'Event 2', - type: 'Meeting', - location: 'In person', - time: '2:00 PM', - date: new Date(2025, 0, 31), - status: 'Needs Attendees', - description: 'Detailed description of Event 2.', - registeredCount: 2, - capacity: 10, - }, - { - id: 3, - title: 'Event 3', - type: 'Workshop', - location: 'Virtual', - time: '12:00 PM', - date: new Date(2025, 0, 28), - status: 'New', - description: 'Detailed description of Event 3.', - registeredCount: 1, - capacity: 100, - }, - { - id: 4, - title: 'Event 4 (Full)', - type: 'Webinar', - location: 'Virtual', - time: '3:00 AM', - date: new Date(2025, 0, 3), - status: 'Full', - description: 'Detailed description of Event 4.', - registeredCount: 10, - capacity: 10, - }, - { - id: 5, - title: 'Event 5', - type: 'Social Gathering', - location: 'In person', - time: '11:00 AM', - date: new Date(2025, 0, 28), - status: 'Filling Fast', - description: 'Detailed description of Event 5.', - registeredCount: 49, - capacity: 50, - }, -]; - -const STATUSES = ['New', 'Needs Attendees', 'Filling Fast', 'Full Event']; -const EVENT_TYPES = ['Workshop', 'Webinar', 'Meeting', 'Social Gathering']; -const LOCATIONS = ['Virtual', 'In person']; -const TIMES = ['10:00 AM', '1:00 PM', '3:00 PM', '5:00 PM']; - -function CommunityCalendar() { - const [filter, setFilter] = useState({ type: 'all', location: 'all', status: 'all' }); - const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedEvent, setSelectedEvent] = useState(null); - const [showEventModal, setShowEventModal] = useState(false); - const [events, setEvents] = useState(mockEvents); - - // Derive status from count/capacity - const getDerivedStatus = useCallback(event => { - if (event.registeredCount === undefined || !event.capacity) { - return event.status; - } - - const count = event.registeredCount; - const capacity = event.capacity; +const MOCK_EVENTS = []; - if (count >= capacity) return 'Full'; // max capacity - if (count >= capacity * 0.7) return 'Filling Fast'; // 70% threshold - if (capacity * 0.1 < count && count <= capacity * 0.2) return 'Needs Attendees'; // between 10% and 20% of capacity +const normalizeStatus = status => { + if (!status) return 'New'; - return 'New'; - }, []); + const s = status.toLowerCase(); - const getDerivedStatusClassNames = event => { - const status = getDerivedStatus(event); - if (status === 'Full') return 'Full'; - if (status === 'Filling Fast') return 'FillingFast'; - if (status === 'Needs Attendees') return 'NeedsAttendees'; - if (status === 'New') return 'New'; - return ''; - }; + if (s.includes('need')) return 'Needs Attendees'; + if (s.includes('fill')) return 'Filling Fast'; + if (s.includes('full')) return 'Full Event'; + if (s.includes('new')) return 'New'; - // Function to determine the CSS class based on the derived status - const getEventStatusClass = useCallback(dynamicStatus => { - let statusClassName; - - switch (dynamicStatus) { - case 'Full': - statusClassName = styles.statusFull; - break; - case 'Filling Fast': - statusClassName = styles.statusFillingFast; - break; - case 'New': - statusClassName = styles.statusNew; - break; - case 'Needs Attendees': - statusClassName = styles.statusNeedsAttendees; - break; - default: - statusClassName = ''; - } - return statusClassName; - }, []); + return 'New'; +}; - // Handler for registration button click - const handleRegister = useCallback( - eventToRegister => { - if (getDerivedStatus(eventToRegister) === 'Full') { - alert('This event is full!'); - return; - } - - const updatedEvent = { - ...eventToRegister, - registeredCount: (eventToRegister.registeredCount || 0) + 1, - }; +export default function CommunityCalendar() { + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [filter, setFilter] = useState({ type: 'all', location: 'all', status: 'all' }); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [selectedEvent, setSelectedEvent] = useState(null); + const [showEventModal, setShowEventModal] = useState(false); + const [hoveredEventId, setHoveredEventId] = useState(null); + const darkMode = useSelector(state => state.theme.darkMode); + const currentDate = new Date(); - // Changing event status to "Full" when capacity is reached and Event Title to match it - if (updatedEvent.registeredCount === updatedEvent.capacity) { - updatedEvent.status = 'Full'; - updatedEvent.title = updatedEvent.title + ' (Full)'; + useEffect(() => { + const fetchEvents = async () => { + setIsLoading(true); + try { + const response = await axios.get(ENDPOINTS.EVENTS); + + const apiEvents = response.data?.events || response.data || []; + + if (!apiEvents || apiEvents.length === 0) { + console.warn('API returned empty → using mock events'); + setEvents(MOCK_EVENTS); + } else { + setEvents(apiEvents); + } + } catch (err) { + console.warn('API failed → using mock events'); + setEvents(MOCK_EVENTS); + } finally { + setIsLoading(false); } + }; - setEvents(prevEvents => - prevEvents.map(ev => (ev.id === eventToRegister.id ? updatedEvent : ev)), - ); + fetchEvents(); + }, []); - setSelectedEvent(updatedEvent); + const mappedEvents = useMemo(() => { + return events.map(event => { + const eventDate = new Date(event.date); + const timeString = eventDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + return { + ...event, + id: event.id || `${event.title}-${eventDate.getTime()}`, + date: eventDate, + type: event.type || 'General', + status: normalizeStatus(event.status), + time: event.time || timeString, + description: event.description || `Join us for ${event.title}`, + location: event.location || 'Online', + }; + }); + }, [events]); - console.log( - `Registered for ${updatedEvent.title}! Count updated to ${updatedEvent.registeredCount}.`, - ); - }, - [getDerivedStatus], + const filteredEvents = useMemo( + () => + mappedEvents.filter( + e => + (filter.type === 'all' || e.type === filter.type) && + (filter.location === 'all' || e.location === filter.location) && + (filter.status === 'all' || e.status === filter.status), + ), + [mappedEvents, filter], ); - // Memoized filtered events - only recalculates when filter or events change - const filteredEvents = useMemo(() => { - return events.filter( - event => - (filter.type === 'all' || event.type === filter.type) && - (filter.location === 'all' || event.location === filter.location) && - (filter.status === 'all' || getDerivedStatus(event) === filter.status), - ); - }, [filter, events, getDerivedStatus]); - // Enhanced event caching by date - memoized for performance const eventCache = useMemo(() => { const map = new Map(); @@ -192,18 +110,6 @@ function CommunityCalendar() { return map; }, [filteredEvents]); - // Memoized unique filter values for dropdowns - const uniqueFilterValues = useMemo(() => { - const types = [...new Set(events.map(event => event.type))]; - const locations = [...new Set(events.map(event => event.location))]; - - // Use the dynamic statuses for the filter dropdown - const dynamicStatuses = ['Full', 'Filling Fast', 'Open', 'New', 'Needs Attendees']; - const statuses = [...new Set(dynamicStatuses)]; - - return { types, locations, statuses }; - }, [events]); - const handleFilterChange = useCallback( filterType => e => { setFilter(prev => ({ @@ -219,7 +125,8 @@ function CommunityCalendar() { // Memoized helper function to get events for a specific date const getEventsForDate = useCallback( date => { - return eventCache.get(date.toDateString()) || []; + if (!date) return []; + return eventCache.get(new Date(date).toDateString()) || []; }, [eventCache], ); @@ -257,22 +164,13 @@ function CommunityCalendar() { }, [getEventsForDate], ); - const handleEventClick = useCallback(event => { + setSelectedDate(new Date(event.date)); + setSelectedEvent(event); + setShowEventModal(true); }, []); - - const handleEventKeyPress = useCallback( - (e, event) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleEventClick(event); - } - }, - [handleEventClick], - ); - const closeEventModal = useCallback(() => { setShowEventModal(false); setSelectedEvent(null); @@ -305,24 +203,20 @@ function CommunityCalendar() { useEffect(() => { const eventsForDate = getEventsForDate(selectedDate); + if (eventsForDate.length === 0) { if (selectedEvent !== null) { setSelectedEvent(null); } - if (showEventModal) { - setShowEventModal(false); - } return; } const hasSelectedEvent = eventsForDate.some(event => event.id === selectedEvent?.id); + if (!hasSelectedEvent) { setSelectedEvent(eventsForDate[0]); - if (showEventModal) { - setShowEventModal(false); - } } - }, [getEventsForDate, selectedDate, selectedEvent, showEventModal]); + }, [getEventsForDate, selectedDate]); const statusMap = { New: 'statusNew', @@ -331,43 +225,183 @@ function CommunityCalendar() { 'Full Event': 'statusFull', }; - // Memoized tile content function + const statusIconMap = { + New: '⭐', + 'Needs Attendees': '🙋', + 'Filling Fast': '⚡', + 'Full Event': '⛔', + Full: '⛔', + }; + + function WeeklyTimeGrid({ events, selectedDate, onEventClick, darkMode }) { + const hours = Array.from({ length: 24 }, (_, i) => i); + + const startOfWeek = useMemo(() => { + const d = new Date(selectedDate); + d.setDate(d.getDate() - d.getDay()); + return d; + }, [selectedDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const day = new Date(startOfWeek); + day.setDate(startOfWeek.getDate() + i); + return day; + }); + }, [startOfWeek]); + + return ( +
+
+
+ {weekDays.map(date => ( +
+
+ {date.toLocaleDateString('en-US', { weekday: 'short' })} +
+
+ {date.getDate()} +
+
+ ))} +
+ +
+ {hours.map(hour => ( +
+
+ {hour === 0 + ? '12 AM' + : hour > 12 + ? `${hour - 12} PM` + : hour === 12 + ? '12 PM' + : `${hour} AM`} +
+ + {weekDays.map(date => { + const cellEvents = events.filter(e => { + const eventDate = new Date(e.date); + const [hStr] = e.time.split(':'); + let h = parseInt(hStr, 10); + const isPM = e.time.toLowerCase().includes('pm'); + const isAM = e.time.toLowerCase().includes('am'); + if (isPM && h !== 12) h += 12; + if (isAM && h === 12) h = 0; + + return eventDate.toDateString() === date.toDateString() && h === hour; + }); + + return ( +
+ {cellEvents.map(ev => ( + + ))} +
+ ); + })} +
+ ))} +
+
+ ); + } + + // Render event tiles const tileContent = useCallback( ({ date, view }) => { - if (view === 'month') { - const eventsForTile = getEventsForDate(date).map(event => { - const statusClass = getEventStatusClass(getDerivedStatus(event)); - return ( -
handleEventClick(event)} - onKeyDown={e => handleEventKeyPress(e, event)} - role="button" - tabIndex={0} - aria-label={`Click to view details for ${event.title}`} - style={{ - borderRadius: '4px', - padding: '2px 6px', - }} + if (view !== 'month') return null; + const events = getEventsForDate(date); + if (!events.length) return null; + + const visible = events.slice(0, 3); + const hiddenCount = events.length - 3; + + return ( +
+ {visible.map(e => { + const statusKey = statusMap[e.status] || 'statusNew'; + + return ( + + ); + })} + {hiddenCount > 0 && ( +
- ); - }); - return eventsForTile.length > 0 ? ( -
{eventsForTile}
- ) : null; - } - return null; + +{hiddenCount} more + + )} +
+ ); }, - [ - getEventsForDate, - handleEventClick, - handleEventKeyPress, - getEventStatusClass, - getDerivedStatus, - ], + [getEventsForDate, handleEventClick, darkMode, hoveredEventId], ); // Memoized tile class name function @@ -390,12 +424,14 @@ function CommunityCalendar() { setFilter(prev => ({ ...prev, type: e.target.value })); }, []); */ - /* const handleLocationFilterChange = useCallback(e => { - setFilter(prev => ({ ...prev, location: e.target.value })); - }, []); */ - - // Memoized dark mode selector - const darkMode = useSelector(state => state.theme.darkMode); + const uniqueFilterValues = useMemo( + () => ({ + types: [...new Set(mappedEvents.map(e => e.type))], + locations: [...new Set(mappedEvents.map(e => e.location))], + statuses: [...new Set(mappedEvents.map(e => e.status))], + }), + [mappedEvents], + ); // Memoized CSS classes const calendarClasses = useMemo( @@ -537,7 +573,11 @@ function CommunityCalendar() {
- +

{event.title}

-
+
  • @@ -608,7 +647,9 @@ function CommunityCalendar() { icon={faCircleCheck} className={styles.metaIcon} /> - {event.status} + + {statusIconMap[event.status] || ''} {event.status} +
@@ -649,11 +690,16 @@ function CommunityCalendar() { ))}
@@ -685,71 +731,47 @@ function CommunityCalendar() {
-
-
- - {getDerivedStatus(selectedEvent)} - -
+
+ + {statusIconMap[selectedEvent.status] || ''} {selectedEvent.status} + +
-
-
- - {selectedEvent.type} -
-
- - {selectedEvent.location} -
-
- - {selectedEvent.date.toLocaleDateString()} -
-
- - {selectedEvent.time} -
- {/* Display count/capacity for context */} -
- - - {selectedEvent.registeredCount || 0} / {selectedEvent.capacity || 'N/A'} - -
+
+
+ Type: + {selectedEvent.type}
- -
- -

{selectedEvent.description}

+
+ Location: + {selectedEvent.location} +
+
+ Date: + {selectedEvent.date.toLocaleDateString()} +
+
+ Time: + {selectedEvent.time}
-
- - +
+ Description: +

{selectedEvent.description}

+ +
+ + +
)}
); } - -export default CommunityCalendar; diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css index 246a997d51..932fa20c97 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css @@ -15,6 +15,7 @@ .calendarActivitySection, .calendarSection { + position: relative; flex: 1; border-radius: 8px; padding: 20px; @@ -60,6 +61,8 @@ } .reactCalendar { + position: relative; + z-index: 1; width: 100%; height: auto; font-size: 1.2rem; @@ -144,17 +147,17 @@ height: 80px; overflow: auto; border: 1px solid #555454; - box-sizing: border-box; + box-sizing: border-box; + vertical-align: top; + padding: 2px; } .reactCalendar :global(.react-calendar__navigation) { background-color: #f9f9f9; border-bottom: 1px solid #ddd; margin-bottom: 0 !important; - } - .reactCalendar :global(.react-calendar__tile) { display: grid; height: 100px; @@ -213,38 +216,37 @@ } .reactCalendar :global(.react-calendar__tile) .tileEvents { - font-size: 0.9rem; - color: #555; - margin-top: 4px; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; - line-height: 1.1; + width: 100%; } -.reactCalendar :global(.react-calendar__tile) :global(.eventItem) { - background-color: #e0f7fa; - padding: 5px; - border-radius: 4px; - margin-bottom: 5px; - white-space: nowrap; +.tileEvents { + position: relative; } /* Ensure tileEvents text is readable on selected date */ -.reactCalendar :global(.react-calendar__tile.selectedDate) .tileEvents { - color: #1a202c; -} .reactCalendarDarkMode :global(.react-calendar__tile) .tileEvents { color: #e2e8f0; } +.reactCalendar :global(.react-calendar__tile.selectedDate) .tileEvents { + color: #1a202c; +} + .reactCalendarDarkMode :global(.react-calendar__tile.selectedDate) .tileEvents { color: #fff; } - +.eventItem { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .reactCalendar .eventItem { display: block; @@ -255,9 +257,20 @@ border-radius: 3px; background-color: #e0f7fa; cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +} + +.reactCalendar :global(.eventItem) { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + padding: 2px 6px; + margin-bottom: 2px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + background-color: #e0f7fa; + cursor: pointer; } .reactCalendar :global(.react-calendar__tile) .eventItem.statusNew { @@ -394,8 +407,23 @@ color: #ffcdd2; } -.reactCalendar .eventItem.clickable:hover { - transform: scale(1.05) !important; +.overflowPopup .eventItem { + margin-bottom: 8px; + font-size: 0.9rem; + padding: 6px 10px; + border-radius: 6px; + color: #fff; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease; +} + +.overflowPopup .eventItem:hover { + transform: translateY(-1px); +} + +.reactCalendar .eventItem:hover { + transform: translate(-1px); z-index: 10; box-shadow: 0 2px 8px rgb(0 0 0 / 15%); } @@ -501,6 +529,18 @@ align-items: flex-start; } +.modalHeader h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; +} + +.calendarActivitySectionDarkMode h2 { + color: #fff; + margin-bottom: 10px; +} + .selectedDatePanelDarkMode .selectedDateHeader h2 { color: #f7fafc; } @@ -628,22 +668,22 @@ .eventTooltip { position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - margin-bottom: 5px; + top: 0; + left: 100%; + transform: translate(8px, -10px); + z-index: 999; width: 220px; padding: 10px; background-color: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 12px rgb(0 0 0 / 15%); - z-index: 10; - pointer-events: none; display: flex; flex-direction: column; gap: 4px; - color: #333; + font-size: 0.75rem; + line-height: 1.3; + pointer-events: none; } .eventTooltipDark { @@ -674,10 +714,10 @@ justify-content: center; align-items: center; z-index: 1000; - animation: fadeIn 0.2s ease-out; + animation: fade-in 0.2s ease-out; } -@keyframes fadeIn { +@keyframes fade-in { from { opacity: 0; } @@ -700,7 +740,7 @@ z-index: 2; } -@keyframes slideIn { +@keyframes slide-in { from { opacity: 0; transform: translateY(-20px) scale(0.95); @@ -712,11 +752,12 @@ } } -/* .eventModalDark { +.eventModalDark { background: #1a202c; color: #e2e8f0; border: 1px solid #4a5568; -} */ + box-shadow: 0 10px 40px rgb(0 0 0 / 60%); +} .modalHeader { display: flex; @@ -732,13 +773,6 @@ border-radius: 12px 12px 0 0; } -.modalHeader h2 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; - color: #2d3748; -} - .eventModalDark .modalHeader h2 { color: #fff; } @@ -883,6 +917,16 @@ letter-spacing: 0.5px; } +.eventDescription label { + display: block; + font-size: 0.8rem; + font-weight: 600; + color: #718096; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + .eventModalDark .detailItem label { color: #cbd5e0; } @@ -901,16 +945,6 @@ margin-bottom: 24px; } -.eventDescription label { - display: block; - font-size: 0.8rem; - font-weight: 600; - color: #718096; - margin-bottom: 8px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - .eventModalDark .eventDescription label { color: #cbd5e0; } @@ -922,6 +956,11 @@ color: #4a5568; } +.activityNoEventsMessage p { + margin: 0; + font-size: 1rem; +} + .eventModalDark .eventDescription p { color: #f7fafc; } @@ -988,7 +1027,6 @@ background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%); } -/* Better dark mode contrast for all text elements */ .eventModalDark * { text-shadow: 0 1px 2px rgb(0 0 0 / 10%); } @@ -1025,11 +1063,6 @@ color: #f7fafc; } -.calendarActivitySectionDarkMode h2 { - color: #fff; - margin-bottom: 10px; -} - .calendarActivityList { list-style: none; padding: 0; @@ -1074,6 +1107,47 @@ color: #fff; } +.eventButton { + width: 100%; + text-align: left; + background: none; + border: none; + padding: 0; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.eventButton:hover { + background-color: #f0f0f0; +} + +.calendarActivityItemDarkMode .eventButton:hover { + background-color: #4a5568; +} + +.calendarActivityItem.clickable { + cursor: pointer; + transition: background-color 0.2s ease; +} + +.calendarActivityItem.clickable:hover { + background-color: #f0f0f0; +} + +.calendarActivityItemDarkMode.clickable:hover { + background-color: #4a5568; +} + +.activityNoEventsMessage { + padding: 20px; + text-align: center; + color: #666; +} + +.calendarActivitySectionDarkMode .activityNoEventsMessage { + color: #cbd5e0; +} + .activityMessage { margin: 0 0 4px; color: #1a202c; @@ -1155,6 +1229,16 @@ } .reactCalendarDarkMode :global(.react-calendar__navigation) { + background-color: #1b2a41; + border-bottom: 1px solid #444; +} + +.reactCalendarDarkMode :global(.react-calendar__navigation button) { + color: #fff; +} + +.reactCalendarDarkMode :global(.react-calendar__navigation button:enabled:hover), +.reactCalendarDarkMode :global(.react-calendar__navigation button:enabled:focus) { background-color: #2d3748; border-bottom-color: #4a5568; } @@ -1280,21 +1364,6 @@ border-bottom-color: #4a5568; } -.overflowPopup .eventItem { - margin-bottom: 8px; - font-size: 0.9rem; - padding: 6px 10px; - border-radius: 6px; - color: #fff; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s ease; -} - -.overflowPopup .eventItem:hover { - transform: translateY(-1px); -} - .detailLabel { font-size: 0.8rem; font-weight: 600; @@ -1305,7 +1374,23 @@ display: block; } +/* Week Grid Styles */ +.weekGridContainer { + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + height: 700px; + overflow: hidden; + margin-bottom: 20px; +} +.weekGridBody { + flex: 1; + overflow-y: scroll; + background: #fff; +} @media (width <= 1024px) { .calendarContainer{ @@ -1318,3 +1403,136 @@ } +.dateLabel { + font-size: 1.1rem; + font-weight: 600; + color: #1e293b; +} + +.hourRow { + display: grid; + grid-template-columns: 80px repeat(7, 1fr); + min-height: 80px; + border-bottom: 1px solid #f1f5f9; +} + +.timeLabel { + font-size: 0.75rem; + color: #94a3b8; + text-align: right; + padding-right: 12px; + padding-top: 8px; + font-weight: 500; + border-right: 1px solid #e2e8f0; +} + +.gridCell { + border-left: 1px solid #f1f5f9; + position: relative; + padding: 4px; + transition: background 0.2s; +} + +.gridCell:hover { + background-color: #f8fafc; +} + +.gridEvent { + background: #dcfce7; + border-left: 4px solid #22c55e; + border-radius: 6px; + padding: 6px; + font-size: 0.75rem; + cursor: pointer; + box-shadow: 0 1px 2px rgb(0 0 0 / 5%); + margin-bottom: 4px; +} + +.gridEventTitle { + font-weight: 600; + color: #166534; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Dark Mode Support */ +.weekGridContainerDark { + background: #1a2332; + border-color: #2d3748; +} + +.weekGridContainerDark .weekGridBody { + background: #0f172a; +} + +.weekGridHeaderDark { + background: #232d3f; + border-bottom-color: #2d3748; +} + +.dayLabelDark { + color: #a0aec0; +} + +.dateLabelDark { + color: #fff; +} + +.hourRowDark { + border-bottom-color: #2d3748; +} + +.timeLabelDark { + color: #718096; + border-right-color: #2d3748; +} + +.gridCellDark { + border-left-color: #2d3748; +} + +.gridCellDark:hover { + background-color: rgb(255 255 255 / 3%); +} + +.gridEventDark { + background: #064e3b; + border-left: 4px solid #10b981; + box-shadow: 0 2px 4px rgb(0 0 0 / 30%); +} + +.gridEventTitleDark { + color: #d1fae5; +} + +.gridEventTimeDark { + color: #a7f3d0; +} + +.eventContent { + display: flex; + align-items: center; + gap: 4px; + overflow: hidden; +} + +.eventIcon { + font-size: 0.75rem; + flex-shrink: 0; + display: inline-flex; + align-items: center; +} + +.eventTitleText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statusInline { + display: inline-flex; + align-items: center; + gap: 4px; + font-weight: 600; +}