From b9d7a8fa6bfc07574b59afcc3f2734767af4a29e Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Thu, 25 Sep 2025 14:57:04 +0700 Subject: [PATCH] feat: enhance calendar with view sync, Redux fixes, and dynamic slot label dimming - Sync calendar view state between Calendar and Menubar components - Fix Redux serialization errors by using string dates instead of Date objects - Add null checks for eventResize handler to prevent undefined errors - Implement dynamic slot label dimming near current time indicator - Slot labels fade to opacity 0.2 when now indicator is within 15 minutes - Add smooth CSS transitions for better user experience - Handle edge cases around midnight for proper time calculation - Update slot label visibility on view changes and time updates --- src/components/Calendar/Calendar.tsx | 68 +++++++++++++++++++--- src/components/Calendar/CustomCalendar.css | 5 ++ src/components/Event/EventDuplicate.tsx | 4 +- src/features/Events/EventModal.tsx | 6 +- src/features/Events/EventsTypes.ts | 6 +- 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 9329651..1a6d475 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -38,6 +38,37 @@ import { userAttendee } from "../../features/User/userDataTypes"; import { TempCalendarsInput } from "./TempCalendarsInput"; import Button from "@mui/material/Button"; +// Function to hide/show slot labels based on current time +const updateSlotLabelVisibility = (currentTime: Date) => { + const slotLabels = document.querySelectorAll(".fc-timegrid-slot-label"); + const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes(); + + slotLabels.forEach((label) => { + const labelElement = label as HTMLElement; + const timeText = labelElement.textContent?.trim(); + + if (timeText && timeText.match(/^\d{1,2}:\d{2}$/)) { + const [hours, minutes] = timeText.split(':').map(Number); + const labelMinutes = hours * 60 + minutes; + + // Calculate time difference in minutes + let timeDiff = Math.abs(currentMinutes - labelMinutes); + + // Handle edge case around midnight (00:00) + if (timeDiff > 12 * 60) { // More than 12 hours difference + timeDiff = 24 * 60 - timeDiff; // Wrap around + } + + // Dim if within 15 minutes (before or after) + if (timeDiff <= 15) { + labelElement.style.opacity = '0.2'; + } else { + labelElement.style.opacity = '1'; + } + } + }); +}; + interface CalendarAppProps { calendarRef: React.RefObject; onDateChange?: (date: Date) => void; @@ -390,6 +421,11 @@ export default function CalendarApp({ if (onViewChange) { onViewChange(arg.view.type); } + + // Update slot label visibility when view changes + setTimeout(() => { + updateSlotLabelVisibility(new Date()); + }, 100); }} dayHeaderContent={(arg) => { const date = arg.date.getDate(); @@ -444,7 +480,7 @@ export default function CalendarApp({ } }} viewDidMount={(arg) => { - // Update now indicator arrow with current time + // Update now indicator arrow with current time and hide nearby slot labels const updateNowIndicator = () => { const nowIndicatorArrow = document.querySelector( ".fc-timegrid-now-indicator-arrow" @@ -457,6 +493,9 @@ export default function CalendarApp({ hour12: false, }); nowIndicatorArrow.setAttribute("data-time", timeString); + + // Hide slot labels that are too close to current time + updateSlotLabelVisibility(now); } }; @@ -464,7 +503,7 @@ export default function CalendarApp({ updateNowIndicator(); const timeInterval = setInterval(updateNowIndicator, 60000); - // Watch for now indicator arrow creation and update immediately + // Watch for now indicator arrow creation and slot label changes const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { @@ -476,9 +515,14 @@ export default function CalendarApp({ ) || element.querySelector?.( ".fc-timegrid-now-indicator-arrow" - ) + ) || + element.classList?.contains("fc-timegrid-slot-label") || + element.querySelector?.(".fc-timegrid-slot-label") ) { - setTimeout(updateNowIndicator, 10); + setTimeout(() => { + updateNowIndicator(); + updateSlotLabelVisibility(new Date()); + }, 10); } } }); @@ -656,6 +700,10 @@ export default function CalendarApp({ return true; }} eventDrop={(arg) => { + if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) { + return; + } + const event = calendars[arg.event._def.extendedProps.calId].events[ arg.event._def.extendedProps.uid @@ -673,8 +721,8 @@ export default function CalendarApp({ ); const newEvent = { ...event, - start: computedNewStart, - end: computedNewEnd, + start: computedNewStart.toISOString(), + end: computedNewEnd.toISOString(), } as CalendarEvent; dispatch( updateEventLocal({ calId: newEvent.calId, event: newEvent }) @@ -684,6 +732,10 @@ export default function CalendarApp({ ); }} eventResize={(arg) => { + if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) { + return; + } + const event = calendars[arg.event._def.extendedProps.calId].events[ arg.event._def.extendedProps.uid @@ -701,8 +753,8 @@ export default function CalendarApp({ ); const newEvent = { ...event, - start: computedNewStart, - end: computedNewEnd, + start: computedNewStart.toISOString(), + end: computedNewEnd.toISOString(), } as CalendarEvent; dispatch( diff --git a/src/components/Calendar/CustomCalendar.css b/src/components/Calendar/CustomCalendar.css index 67683b0..c33b828 100644 --- a/src/components/Calendar/CustomCalendar.css +++ b/src/components/Calendar/CustomCalendar.css @@ -17,6 +17,11 @@ a.fc-timegrid-axis-cushion { line-height: 18.76px; letter-spacing: 0.391px; } + +/* Slot label visibility transitions */ +.fc-timegrid-slot-label { + transition: opacity 0.3s ease-in-out; +} .fc-daygrid-day-top small { color: #8c9caf; font-size: 14px; diff --git a/src/components/Event/EventDuplicate.tsx b/src/components/Event/EventDuplicate.tsx index 9e5d453..af02c3a 100644 --- a/src/components/Event/EventDuplicate.tsx +++ b/src/components/Event/EventDuplicate.tsx @@ -15,9 +15,9 @@ export default function EventDuplication({ const [openModal, setOpenModal] = useState(false); const [selectedRange, setSelectedRange] = useState({ start: new Date(event.start), - startStr: new Date(event.start).toISOString(), + startStr: event.start, end: new Date(event.end ?? ""), - endStr: new Date(event.end ?? "").toISOString(), + endStr: event.end ?? "", allDay: event.allday ?? false, } as DateSelectArg); const calendarRef = useRef(null); diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 1822927..ea75819 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -71,12 +71,10 @@ function EventPopover({ const [description, setDescription] = useState(event?.description ?? ""); const [location, setLocation] = useState(event?.location ?? ""); const [start, setStart] = useState( - event?.start - ? new Date(event.start).toISOString() - : new Date().toISOString() + event?.start ? event.start : "" ); const [end, setEnd] = useState( - event?.end ? new Date(event.end)?.toISOString() : new Date().toISOString() + event?.end ? event.end : "" ); const [calendarid, setCalendarid] = useState( event?.calId diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 94d1ee8..13105a3 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -5,8 +5,8 @@ export interface CalendarEvent { calId: string; uid: string; transp?: string; - start: Date; // ISO date - end?: Date; + start: string; // ISO date string + end?: string; class?: string; x_openpass_videoconference?: string; title?: string; @@ -14,7 +14,7 @@ export interface CalendarEvent { location?: string; organizer?: userOrganiser; attendee: userAttendee[]; - stamp?: Date; + stamp?: string; sequence?: Number; color?: string; allday?: boolean;