refactor: extract calendar utility functions and handlers
- Extract utility functions to utils/calendarUtils.ts * updateSlotLabelVisibility for slot label management * eventToFullCalendarFormat for event formatting * extractEvents for event filtering * updateCalsDetails for calendar detail updates - Extract event handlers to handlers/eventHandlers.ts * handleDateSelect, handleClosePopover, handleEventClick * handleEventAllow, handleEventDrop, handleEventResize - Extract mouse handlers to handlers/mouseHandlers.ts * handleMouseMove, handleMouseLeave * addMouseEventListeners, removeMouseEventListeners - Extract view handlers to handlers/viewHandlers.ts * handleViewDidMount, handleViewWillUnmount * handleEventContent, handleEventDidMount * handleDayHeaderDidMount, handleDayHeaderWillUnmount - Remove inline functions from Calendar.tsx - Improve code organization and maintainability
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
|
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
@@ -37,38 +36,14 @@ import LockIcon from "@mui/icons-material/Lock";
|
|||||||
import { userAttendee } from "../../features/User/userDataTypes";
|
import { userAttendee } from "../../features/User/userDataTypes";
|
||||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
import {
|
||||||
// Function to hide/show slot labels based on current time
|
updateSlotLabelVisibility,
|
||||||
const updateSlotLabelVisibility = (currentTime: Date) => {
|
eventToFullCalendarFormat,
|
||||||
const slotLabels = document.querySelectorAll(".fc-timegrid-slot-label");
|
extractEvents,
|
||||||
const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
updateCalsDetails,
|
||||||
|
} from "./utils/calendarUtils";
|
||||||
slotLabels.forEach((label) => {
|
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
||||||
const labelElement = label as HTMLElement;
|
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
||||||
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 {
|
interface CalendarAppProps {
|
||||||
calendarRef: React.RefObject<CalendarApi | null>;
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
@@ -84,20 +59,20 @@ export default function CalendarApp({
|
|||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||||
const tokens = useAppSelector((state) => state.user.tokens);
|
const tokens = useAppSelector((state) => state.user.tokens);
|
||||||
|
const userId =
|
||||||
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tokens || !user) {
|
if (!tokens || !userId) {
|
||||||
dispatch(push("/"));
|
dispatch(push("/"));
|
||||||
}
|
}
|
||||||
}, [tokens, user]);
|
}, [tokens, userId]);
|
||||||
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
const tempcalendars =
|
const tempcalendars =
|
||||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||||
const pending = useAppSelector((state) => state.calendars.pending);
|
const pending = useAppSelector((state) => state.calendars.pending);
|
||||||
const userId =
|
|
||||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
|
||||||
const selectPersonnalCalendars = createSelector(
|
const selectPersonnalCalendars = createSelector(
|
||||||
(state) => state.calendars,
|
(state) => state.calendars,
|
||||||
(calendars) =>
|
(calendars) =>
|
||||||
@@ -207,53 +182,39 @@ export default function CalendarApp({
|
|||||||
{} as CalendarEvent
|
{} as CalendarEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
// Event handlers
|
||||||
setSelectedRange(selectInfo);
|
const eventHandlers = useCalendarEventHandlers({
|
||||||
setAnchorEl(document.body); // fallback: we could use selectInfo.jsEvent.target if from a click
|
setSelectedRange,
|
||||||
};
|
setAnchorEl,
|
||||||
|
calendarRef,
|
||||||
|
selectedCalendars,
|
||||||
|
tempcalendars,
|
||||||
|
calendarRange,
|
||||||
|
dispatch,
|
||||||
|
setOpenEventDisplay,
|
||||||
|
setAnchorPosition,
|
||||||
|
setEventDisplayedId,
|
||||||
|
setEventDisplayedCalId,
|
||||||
|
setEventDisplayedTemp,
|
||||||
|
calendars,
|
||||||
|
});
|
||||||
|
|
||||||
const handleClosePopover = () => {
|
// View handlers
|
||||||
calendarRef.current?.unselect();
|
const viewHandlers = useCalendarViewHandlers({
|
||||||
setAnchorEl(null);
|
calendarRef,
|
||||||
setSelectedRange(null);
|
setSelectedDate,
|
||||||
selectedCalendars.forEach((calId) =>
|
setSelectedMiniDate,
|
||||||
dispatch(
|
onViewChange,
|
||||||
getCalendarDetailAsync({
|
calendars,
|
||||||
calId,
|
tempcalendars,
|
||||||
match: {
|
});
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Object.keys(tempcalendars).forEach((calId) =>
|
|
||||||
dispatch(
|
|
||||||
getCalendarDetailAsync({
|
|
||||||
calId,
|
|
||||||
match: {
|
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
||||||
},
|
|
||||||
calType: "temp",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const handleCloseEventDisplay = () => {
|
|
||||||
setAnchorPosition(null);
|
|
||||||
setOpenEventDisplay(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMonthUp = () => {
|
const handleMonthUp = () => {
|
||||||
setSelectedMiniDate(
|
eventHandlers.handleMonthUp(selectedMiniDate, setSelectedMiniDate);
|
||||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMonthDown = () => {
|
const handleMonthDown = () => {
|
||||||
setSelectedMiniDate(
|
eventHandlers.handleMonthDown(selectedMiniDate, setSelectedMiniDate);
|
||||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "test") {
|
if (process.env.NODE_ENV === "test") {
|
||||||
@@ -265,7 +226,9 @@ export default function CalendarApp({
|
|||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => handleDateSelect(null as unknown as DateSelectArg)}
|
onClick={() =>
|
||||||
|
eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AddIcon /> <p>Create Event</p>
|
<AddIcon /> <p>Create Event</p>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -378,7 +341,7 @@ export default function CalendarApp({
|
|||||||
selectable={true}
|
selectable={true}
|
||||||
timeZone="local"
|
timeZone="local"
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
select={handleDateSelect}
|
select={eventHandlers.handleDateSelect}
|
||||||
nowIndicator
|
nowIndicator
|
||||||
headerToolbar={false}
|
headerToolbar={false}
|
||||||
views={{
|
views={{
|
||||||
@@ -448,433 +411,21 @@ export default function CalendarApp({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
dayHeaderDidMount={(arg) => {
|
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
||||||
// Add click handler to day headers in week view
|
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
||||||
if (arg.view.type === "timeGridWeek") {
|
viewDidMount={viewHandlers.handleViewDidMount}
|
||||||
const headerEl = arg.el;
|
viewWillUnmount={viewHandlers.handleViewWillUnmount}
|
||||||
|
eventClick={eventHandlers.handleEventClick}
|
||||||
const handleDayHeaderClick = () => {
|
eventAllow={eventHandlers.handleEventAllow}
|
||||||
// Switch to day view and navigate to the clicked date
|
eventDrop={eventHandlers.handleEventDrop}
|
||||||
calendarRef.current?.changeView("timeGridDay", arg.date);
|
eventResize={eventHandlers.handleEventResize}
|
||||||
setSelectedDate(new Date(arg.date));
|
eventContent={viewHandlers.handleEventContent}
|
||||||
setSelectedMiniDate(new Date(arg.date));
|
eventDidMount={viewHandlers.handleEventDidMount}
|
||||||
|
|
||||||
// Notify parent about view change
|
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange("timeGridDay");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
headerEl.addEventListener("click", handleDayHeaderClick);
|
|
||||||
|
|
||||||
// Store the handler for cleanup
|
|
||||||
(headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
dayHeaderWillUnmount={(arg) => {
|
|
||||||
// Clean up event listeners to prevent memory leaks
|
|
||||||
const headerEl = arg.el;
|
|
||||||
if ((headerEl as any).__dayHeaderClickHandler) {
|
|
||||||
headerEl.removeEventListener(
|
|
||||||
"click",
|
|
||||||
(headerEl as any).__dayHeaderClickHandler
|
|
||||||
);
|
|
||||||
delete (headerEl as any).__dayHeaderClickHandler;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
viewDidMount={(arg) => {
|
|
||||||
// Update now indicator arrow with current time and hide nearby slot labels
|
|
||||||
const updateNowIndicator = () => {
|
|
||||||
const nowIndicatorArrow = document.querySelector(
|
|
||||||
".fc-timegrid-now-indicator-arrow"
|
|
||||||
) as HTMLElement;
|
|
||||||
if (nowIndicatorArrow) {
|
|
||||||
const now = new Date();
|
|
||||||
const timeString = now.toLocaleTimeString(undefined, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
});
|
|
||||||
nowIndicatorArrow.setAttribute("data-time", timeString);
|
|
||||||
|
|
||||||
// Hide slot labels that are too close to current time
|
|
||||||
updateSlotLabelVisibility(now);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update immediately and then every minute
|
|
||||||
updateNowIndicator();
|
|
||||||
const timeInterval = setInterval(updateNowIndicator, 60000);
|
|
||||||
|
|
||||||
// Watch for now indicator arrow creation and slot label changes
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as Element;
|
|
||||||
if (
|
|
||||||
element.classList?.contains(
|
|
||||||
"fc-timegrid-now-indicator-arrow"
|
|
||||||
) ||
|
|
||||||
element.querySelector?.(
|
|
||||||
".fc-timegrid-now-indicator-arrow"
|
|
||||||
) ||
|
|
||||||
element.classList?.contains("fc-timegrid-slot-label") ||
|
|
||||||
element.querySelector?.(".fc-timegrid-slot-label")
|
|
||||||
) {
|
|
||||||
setTimeout(() => {
|
|
||||||
updateNowIndicator();
|
|
||||||
updateSlotLabelVisibility(new Date());
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store interval and observer for cleanup
|
|
||||||
(arg.el as any).__timeInterval = timeInterval;
|
|
||||||
(arg.el as any).__timeObserver = observer;
|
|
||||||
|
|
||||||
// Add global hover effect for week and day views
|
|
||||||
if (
|
|
||||||
arg.view.type === "timeGridWeek" ||
|
|
||||||
arg.view.type === "timeGridDay"
|
|
||||||
) {
|
|
||||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
|
||||||
if (calendarEl) {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
// Find the timegrid container
|
|
||||||
const timegridEl =
|
|
||||||
calendarEl.querySelector(".fc-timegrid-body");
|
|
||||||
if (!timegridEl) return;
|
|
||||||
|
|
||||||
// Check if mouse is over all-day events area (fc-scrollgrid-sync-table)
|
|
||||||
const allDayTable = calendarEl.querySelector(
|
|
||||||
".fc-scrollgrid-sync-table"
|
|
||||||
);
|
|
||||||
if (allDayTable) {
|
|
||||||
const allDayRect = allDayTable.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
e.clientY >= allDayRect.top &&
|
|
||||||
e.clientY <= allDayRect.bottom
|
|
||||||
) {
|
|
||||||
// Mouse is over all-day events area, don't show highlight
|
|
||||||
timegridEl
|
|
||||||
.querySelectorAll(".hour-highlight")
|
|
||||||
.forEach((el: Element) => el.remove());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mouse is over time slot labels (left side with hours)
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target && target.closest(".fc-timegrid-slot-label")) {
|
|
||||||
// Mouse is over time slot labels, don't show highlight
|
|
||||||
timegridEl
|
|
||||||
.querySelectorAll(".hour-highlight")
|
|
||||||
.forEach((el: Element) => el.remove());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all day columns
|
|
||||||
const dayColumns =
|
|
||||||
timegridEl.querySelectorAll(".fc-timegrid-col");
|
|
||||||
if (dayColumns.length === 0) return;
|
|
||||||
|
|
||||||
// Remove previous highlights
|
|
||||||
timegridEl
|
|
||||||
.querySelectorAll(".hour-highlight")
|
|
||||||
.forEach((el: Element) => el.remove());
|
|
||||||
|
|
||||||
// Find which day column the mouse is over
|
|
||||||
let targetColumn: Element | null = null;
|
|
||||||
for (const column of dayColumns) {
|
|
||||||
const rect = column.getBoundingClientRect();
|
|
||||||
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
|
||||||
targetColumn = column;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetColumn) {
|
|
||||||
const rect = targetColumn.getBoundingClientRect();
|
|
||||||
const relativeY = e.clientY - rect.top;
|
|
||||||
const hourHeight = rect.height / 24; // Assuming 24 hours
|
|
||||||
const hourIndex = Math.floor(relativeY / hourHeight);
|
|
||||||
|
|
||||||
// Only show highlight if mouse is actually over the timegrid area (not all-day events)
|
|
||||||
if (relativeY >= 0 && relativeY <= rect.height) {
|
|
||||||
// Create highlight for the specific hour in the specific day
|
|
||||||
const highlight = document.createElement("div");
|
|
||||||
highlight.className = "hour-highlight";
|
|
||||||
highlight.style.top = `${hourIndex * hourHeight}px`;
|
|
||||||
highlight.style.height = `${hourHeight}px`;
|
|
||||||
|
|
||||||
(targetColumn as HTMLElement).style.position = "relative";
|
|
||||||
targetColumn.appendChild(highlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
// Remove all hour highlights when mouse leaves calendar
|
|
||||||
const timegridEl =
|
|
||||||
calendarEl.querySelector(".fc-timegrid-body");
|
|
||||||
if (timegridEl) {
|
|
||||||
timegridEl
|
|
||||||
.querySelectorAll(".hour-highlight")
|
|
||||||
.forEach((el: Element) => el.remove());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
calendarEl.addEventListener("mousemove", handleMouseMove);
|
|
||||||
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
|
||||||
|
|
||||||
// Store handlers for cleanup
|
|
||||||
(calendarEl as any).__calendarMouseMoveHandler =
|
|
||||||
handleMouseMove;
|
|
||||||
(calendarEl as any).__calendarMouseLeaveHandler =
|
|
||||||
handleMouseLeave;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
viewWillUnmount={(arg) => {
|
|
||||||
// Clean up time interval
|
|
||||||
if ((arg.el as any).__timeInterval) {
|
|
||||||
clearInterval((arg.el as any).__timeInterval);
|
|
||||||
delete (arg.el as any).__timeInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up observer
|
|
||||||
if ((arg.el as any).__timeObserver) {
|
|
||||||
(arg.el as any).__timeObserver.disconnect();
|
|
||||||
delete (arg.el as any).__timeObserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up event listeners to prevent memory leaks
|
|
||||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
|
||||||
if (calendarEl) {
|
|
||||||
if ((calendarEl as any).__calendarMouseMoveHandler) {
|
|
||||||
calendarEl.removeEventListener(
|
|
||||||
"mousemove",
|
|
||||||
(calendarEl as any).__calendarMouseMoveHandler
|
|
||||||
);
|
|
||||||
delete (calendarEl as any).__calendarMouseMoveHandler;
|
|
||||||
}
|
|
||||||
if ((calendarEl as any).__calendarMouseLeaveHandler) {
|
|
||||||
calendarEl.removeEventListener(
|
|
||||||
"mouseleave",
|
|
||||||
(calendarEl as any).__calendarMouseLeaveHandler
|
|
||||||
);
|
|
||||||
delete (calendarEl as any).__calendarMouseLeaveHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
eventClick={(info) => {
|
|
||||||
info.jsEvent.preventDefault(); // don't let the browser navigate
|
|
||||||
|
|
||||||
if (info.event.url) {
|
|
||||||
window.open(info.event.url);
|
|
||||||
} else {
|
|
||||||
console.log(info.event);
|
|
||||||
setOpenEventDisplay(true);
|
|
||||||
setAnchorPosition({
|
|
||||||
top: info.jsEvent.clientY,
|
|
||||||
left: info.jsEvent.clientX,
|
|
||||||
});
|
|
||||||
setEventDisplayedId(info.event.extendedProps.uid);
|
|
||||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
|
||||||
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
eventAllow={(dropInfo, draggedEvent) => {
|
|
||||||
if (
|
|
||||||
draggedEvent?.extendedProps.uid &&
|
|
||||||
draggedEvent.extendedProps.uid.split("/")[1]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
];
|
|
||||||
|
|
||||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
|
||||||
|
|
||||||
const originalStart = new Date(event.start);
|
|
||||||
const computedNewStart = new Date(
|
|
||||||
originalStart.getTime() + totalDeltaMs
|
|
||||||
);
|
|
||||||
const originalEnd = new Date(event.end ?? "");
|
|
||||||
const computedNewEnd = new Date(
|
|
||||||
originalEnd.getTime() + totalDeltaMs
|
|
||||||
);
|
|
||||||
const newEvent = {
|
|
||||||
...event,
|
|
||||||
start: computedNewStart.toISOString(),
|
|
||||||
end: computedNewEnd.toISOString(),
|
|
||||||
} as CalendarEvent;
|
|
||||||
dispatch(
|
|
||||||
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
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
|
|
||||||
];
|
|
||||||
if (event.uid.split("/")[1]) {
|
|
||||||
dispatch(getEventAsync(event));
|
|
||||||
}
|
|
||||||
const originalStart = new Date(event.start);
|
|
||||||
const computedNewStart = new Date(
|
|
||||||
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
|
||||||
);
|
|
||||||
const originalEnd = new Date(event.end ?? "");
|
|
||||||
const computedNewEnd = new Date(
|
|
||||||
originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta)
|
|
||||||
);
|
|
||||||
const newEvent = {
|
|
||||||
...event,
|
|
||||||
start: computedNewStart.toISOString(),
|
|
||||||
end: computedNewEnd.toISOString(),
|
|
||||||
} as CalendarEvent;
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
eventContent={(arg) => {
|
|
||||||
const event = arg.event;
|
|
||||||
if (
|
|
||||||
(!event._def.extendedProps.temp &&
|
|
||||||
!calendars[arg.event._def.extendedProps.calId]) ||
|
|
||||||
(event._def.extendedProps.temp &&
|
|
||||||
!tempcalendars[arg.event._def.extendedProps.calId])
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const attendees = event._def.extendedProps.attendee || [];
|
|
||||||
const isPrivate =
|
|
||||||
event._def.extendedProps.class === "PRIVATE" ||
|
|
||||||
event._def.extendedProps.class === "CONFIDENTIAL";
|
|
||||||
let Icon = null;
|
|
||||||
let titleStyle: React.CSSProperties = {};
|
|
||||||
const ownerEmails = new Set(
|
|
||||||
(event._def.extendedProps.temp ? tempcalendars : calendars)[
|
|
||||||
arg.event._def.extendedProps.calId
|
|
||||||
].ownerEmails?.map((email) => email.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const delegated = (
|
|
||||||
event._def.extendedProps.temp ? tempcalendars : calendars
|
|
||||||
)[arg.event._def.extendedProps.calId].delegated;
|
|
||||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
|
||||||
ownerEmails.has(att.cal_address.toLowerCase())
|
|
||||||
);
|
|
||||||
if (!delegated && showSpecialDisplay.length === 0) return null;
|
|
||||||
switch (showSpecialDisplay?.[0]?.partstat) {
|
|
||||||
case "DECLINED":
|
|
||||||
Icon = null;
|
|
||||||
titleStyle.textDecoration = "line-through";
|
|
||||||
break;
|
|
||||||
case "TENTATIVE":
|
|
||||||
Icon = HelpOutlineIcon;
|
|
||||||
break;
|
|
||||||
case "NEEDS-ACTION":
|
|
||||||
Icon = AccessTimeIcon;
|
|
||||||
break;
|
|
||||||
case "ACCEPTED":
|
|
||||||
Icon = null;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
{isPrivate && (
|
|
||||||
<LockIcon
|
|
||||||
data-testid="lock-icon"
|
|
||||||
fontSize="small"
|
|
||||||
style={{ marginRight: "4px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{Icon && (
|
|
||||||
<Icon fontSize="small" style={{ marginRight: "4px" }} />
|
|
||||||
)}
|
|
||||||
<span style={titleStyle}>{event.title}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
eventDidMount={(arg) => {
|
|
||||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
|
||||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
|
||||||
const ownerEmails = new Set(
|
|
||||||
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map(
|
|
||||||
(email) => email.toLowerCase()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
|
||||||
ownerEmails.has(att.cal_address.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!showSpecialDisplay[0]) return;
|
|
||||||
|
|
||||||
// Clear previously applied classes
|
|
||||||
arg.el.classList.remove(
|
|
||||||
"declined-event",
|
|
||||||
"tentative-event",
|
|
||||||
"needs-action-event"
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (showSpecialDisplay[0].partstat) {
|
|
||||||
case "DECLINED":
|
|
||||||
arg.el.classList.add("declined-event");
|
|
||||||
break;
|
|
||||||
case "TENTATIVE":
|
|
||||||
arg.el.classList.add("tentative-event");
|
|
||||||
break;
|
|
||||||
case "NEEDS-ACTION":
|
|
||||||
arg.el.classList.add("needs-action-event");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<EventPopover
|
<EventPopover
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={Boolean(anchorEl)}
|
open={Boolean(anchorEl)}
|
||||||
onClose={handleClosePopover}
|
onClose={eventHandlers.handleClosePopover}
|
||||||
selectedRange={selectedRange}
|
selectedRange={selectedRange}
|
||||||
setSelectedRange={setSelectedRange}
|
setSelectedRange={setSelectedRange}
|
||||||
calendarRef={calendarRef}
|
calendarRef={calendarRef}
|
||||||
@@ -887,72 +438,10 @@ export default function CalendarApp({
|
|||||||
tempEvent={eventDisplayedTemp}
|
tempEvent={eventDisplayedTemp}
|
||||||
anchorPosition={anchorPosition}
|
anchorPosition={anchorPosition}
|
||||||
open={openEventDisplay}
|
open={openEventDisplay}
|
||||||
onClose={handleCloseEventDisplay}
|
onClose={eventHandlers.handleCloseEventDisplay}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventToFullCalendarFormat(
|
|
||||||
filteredEvents: CalendarEvent[],
|
|
||||||
filteredTempEvents: CalendarEvent[],
|
|
||||||
userId: string | undefined
|
|
||||||
) {
|
|
||||||
return filteredEvents
|
|
||||||
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
|
||||||
.map((e) => {
|
|
||||||
if (e.calId.split("/")[0] === userId) {
|
|
||||||
return { ...e, editable: true };
|
|
||||||
}
|
|
||||||
return { ...e, editable: false };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractEvents(
|
|
||||||
selectedCalendars: string[],
|
|
||||||
calendars: Record<string, Calendars>
|
|
||||||
) {
|
|
||||||
let filteredEvents: CalendarEvent[] = [];
|
|
||||||
selectedCalendars.forEach((id) => {
|
|
||||||
if (calendars[id].events) {
|
|
||||||
filteredEvents = filteredEvents
|
|
||||||
.concat(
|
|
||||||
Object.keys(calendars[id].events).map(
|
|
||||||
(eventid) => calendars[id].events[eventid]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.filter((event) => !(event.status === "CANCELLED"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return filteredEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCalsDetails(
|
|
||||||
selectedCalendars: string[],
|
|
||||||
pending: boolean,
|
|
||||||
calendars: Record<string, Calendars>,
|
|
||||||
rangeKey: string,
|
|
||||||
dispatch: Function,
|
|
||||||
calendarRange: { start: Date; end: Date },
|
|
||||||
calType?: "temp"
|
|
||||||
) {
|
|
||||||
selectedCalendars.forEach((id) => {
|
|
||||||
if (Object.keys(calendars[id].events).length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!pending && rangeKey) {
|
|
||||||
dispatch(
|
|
||||||
getCalendarDetailAsync({
|
|
||||||
calId: id,
|
|
||||||
match: {
|
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
||||||
},
|
|
||||||
calType,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
|
import { CalendarApi } from "@fullcalendar/core";
|
||||||
|
import { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||||
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||||
|
import { getDeltaInMilliseconds } from "../../../utils/dateUtils";
|
||||||
|
import {
|
||||||
|
getCalendarDetailAsync,
|
||||||
|
getEventAsync,
|
||||||
|
putEventAsync,
|
||||||
|
updateEventLocal,
|
||||||
|
} from "../../../features/Calendars/CalendarSlice";
|
||||||
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||||
|
|
||||||
|
export interface EventHandlersProps {
|
||||||
|
setSelectedRange: (range: DateSelectArg | null) => void;
|
||||||
|
setAnchorEl: (el: HTMLElement | null) => void;
|
||||||
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
|
selectedCalendars: string[];
|
||||||
|
tempcalendars: Record<string, Calendars>;
|
||||||
|
calendarRange: { start: Date; end: Date };
|
||||||
|
dispatch: any;
|
||||||
|
setOpenEventDisplay: (open: boolean) => void;
|
||||||
|
setAnchorPosition: (position: { top: number; left: number } | null) => void;
|
||||||
|
setEventDisplayedId: (id: string) => void;
|
||||||
|
setEventDisplayedCalId: (id: string) => void;
|
||||||
|
setEventDisplayedTemp: (temp: boolean) => void;
|
||||||
|
calendars: Record<string, Calendars>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEventHandlers = (props: EventHandlersProps) => {
|
||||||
|
const {
|
||||||
|
setSelectedRange,
|
||||||
|
setAnchorEl,
|
||||||
|
calendarRef,
|
||||||
|
selectedCalendars,
|
||||||
|
tempcalendars,
|
||||||
|
calendarRange,
|
||||||
|
dispatch,
|
||||||
|
setOpenEventDisplay,
|
||||||
|
setAnchorPosition,
|
||||||
|
setEventDisplayedId,
|
||||||
|
setEventDisplayedCalId,
|
||||||
|
setEventDisplayedTemp,
|
||||||
|
calendars,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||||
|
setSelectedRange(selectInfo);
|
||||||
|
setAnchorEl(document.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosePopover = () => {
|
||||||
|
calendarRef.current?.unselect();
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedRange(null);
|
||||||
|
selectedCalendars.forEach((calId) =>
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Object.keys(tempcalendars).forEach((calId) =>
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
calType: "temp",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseEventDisplay = () => {
|
||||||
|
setAnchorPosition(null);
|
||||||
|
setOpenEventDisplay(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthUp = (
|
||||||
|
selectedMiniDate: Date,
|
||||||
|
setSelectedMiniDate: (date: Date) => void
|
||||||
|
) => {
|
||||||
|
setSelectedMiniDate(
|
||||||
|
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthDown = (
|
||||||
|
selectedMiniDate: Date,
|
||||||
|
setSelectedMiniDate: (date: Date) => void
|
||||||
|
) => {
|
||||||
|
setSelectedMiniDate(
|
||||||
|
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventClick = (info: any) => {
|
||||||
|
info.jsEvent.preventDefault();
|
||||||
|
|
||||||
|
if (info.event.url) {
|
||||||
|
window.open(info.event.url);
|
||||||
|
} else {
|
||||||
|
console.log(info.event);
|
||||||
|
setOpenEventDisplay(true);
|
||||||
|
setAnchorPosition({
|
||||||
|
top: info.jsEvent.clientY,
|
||||||
|
left: info.jsEvent.clientX,
|
||||||
|
});
|
||||||
|
setEventDisplayedId(info.event.extendedProps.uid);
|
||||||
|
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||||
|
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventAllow = (dropInfo: any, draggedEvent: any) => {
|
||||||
|
if (
|
||||||
|
draggedEvent?.extendedProps.uid &&
|
||||||
|
draggedEvent.extendedProps.uid.split("/")[1]
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventDrop = (arg: any) => {
|
||||||
|
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
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
||||||
|
|
||||||
|
const originalStart = new Date(event.start);
|
||||||
|
const computedNewStart = new Date(originalStart.getTime() + totalDeltaMs);
|
||||||
|
const originalEnd = new Date(event.end ?? "");
|
||||||
|
const computedNewEnd = new Date(originalEnd.getTime() + totalDeltaMs);
|
||||||
|
const newEvent = {
|
||||||
|
...event,
|
||||||
|
start: computedNewStart.toISOString(),
|
||||||
|
end: computedNewEnd.toISOString(),
|
||||||
|
} as CalendarEvent;
|
||||||
|
dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
|
||||||
|
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventResize = (arg: any) => {
|
||||||
|
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
|
||||||
|
];
|
||||||
|
if (event.uid.split("/")[1]) {
|
||||||
|
dispatch(getEventAsync(event));
|
||||||
|
}
|
||||||
|
const originalStart = new Date(event.start);
|
||||||
|
const computedNewStart = new Date(
|
||||||
|
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
||||||
|
);
|
||||||
|
const originalEnd = new Date(event.end ?? "");
|
||||||
|
const computedNewEnd = new Date(
|
||||||
|
originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta)
|
||||||
|
);
|
||||||
|
const newEvent = {
|
||||||
|
...event,
|
||||||
|
start: computedNewStart.toISOString(),
|
||||||
|
end: computedNewEnd.toISOString(),
|
||||||
|
} as CalendarEvent;
|
||||||
|
|
||||||
|
dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDateSelect,
|
||||||
|
handleClosePopover,
|
||||||
|
handleCloseEventDisplay,
|
||||||
|
handleMonthUp,
|
||||||
|
handleMonthDown,
|
||||||
|
handleEventClick,
|
||||||
|
handleEventAllow,
|
||||||
|
handleEventDrop,
|
||||||
|
handleEventResize,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
export interface MouseHandlersProps {
|
||||||
|
calendarEl: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMouseHandlers = (props: MouseHandlersProps) => {
|
||||||
|
const { calendarEl } = props;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||||
|
if (!timegridEl) return;
|
||||||
|
|
||||||
|
const allDayTable = calendarEl.querySelector(".fc-scrollgrid-sync-table");
|
||||||
|
if (allDayTable) {
|
||||||
|
const allDayRect = allDayTable.getBoundingClientRect();
|
||||||
|
if (e.clientY >= allDayRect.top && e.clientY <= allDayRect.bottom) {
|
||||||
|
timegridEl
|
||||||
|
.querySelectorAll(".hour-highlight")
|
||||||
|
.forEach((el: Element) => el.remove());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target && target.closest(".fc-timegrid-slot-label")) {
|
||||||
|
timegridEl
|
||||||
|
.querySelectorAll(".hour-highlight")
|
||||||
|
.forEach((el: Element) => el.remove());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayColumns = timegridEl.querySelectorAll(".fc-timegrid-col");
|
||||||
|
if (dayColumns.length === 0) return;
|
||||||
|
|
||||||
|
timegridEl
|
||||||
|
.querySelectorAll(".hour-highlight")
|
||||||
|
.forEach((el: Element) => el.remove());
|
||||||
|
|
||||||
|
let targetColumn: Element | null = null;
|
||||||
|
for (const column of dayColumns) {
|
||||||
|
const rect = column.getBoundingClientRect();
|
||||||
|
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
||||||
|
targetColumn = column;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetColumn) {
|
||||||
|
const rect = targetColumn.getBoundingClientRect();
|
||||||
|
const relativeY = e.clientY - rect.top;
|
||||||
|
const hourHeight = rect.height / 24;
|
||||||
|
const hourIndex = Math.floor(relativeY / hourHeight);
|
||||||
|
|
||||||
|
if (relativeY >= 0 && relativeY <= rect.height) {
|
||||||
|
const highlight = document.createElement("div");
|
||||||
|
highlight.className = "hour-highlight";
|
||||||
|
highlight.style.top = `${hourIndex * hourHeight}px`;
|
||||||
|
highlight.style.height = `${hourHeight}px`;
|
||||||
|
|
||||||
|
(targetColumn as HTMLElement).style.position = "relative";
|
||||||
|
targetColumn.appendChild(highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||||
|
if (timegridEl) {
|
||||||
|
timegridEl
|
||||||
|
.querySelectorAll(".hour-highlight")
|
||||||
|
.forEach((el: Element) => el.remove());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMouseEventListeners = () => {
|
||||||
|
calendarEl.addEventListener("mousemove", handleMouseMove);
|
||||||
|
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
|
||||||
|
(calendarEl as any).__calendarMouseMoveHandler = handleMouseMove;
|
||||||
|
(calendarEl as any).__calendarMouseLeaveHandler = handleMouseLeave;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMouseEventListeners = () => {
|
||||||
|
if ((calendarEl as any).__calendarMouseMoveHandler) {
|
||||||
|
calendarEl.removeEventListener(
|
||||||
|
"mousemove",
|
||||||
|
(calendarEl as any).__calendarMouseMoveHandler
|
||||||
|
);
|
||||||
|
delete (calendarEl as any).__calendarMouseMoveHandler;
|
||||||
|
}
|
||||||
|
if ((calendarEl as any).__calendarMouseLeaveHandler) {
|
||||||
|
calendarEl.removeEventListener(
|
||||||
|
"mouseleave",
|
||||||
|
(calendarEl as any).__calendarMouseLeaveHandler
|
||||||
|
);
|
||||||
|
delete (calendarEl as any).__calendarMouseLeaveHandler;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMouseMove,
|
||||||
|
handleMouseLeave,
|
||||||
|
addMouseEventListeners,
|
||||||
|
removeMouseEventListeners,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { CalendarApi } from "@fullcalendar/core";
|
||||||
|
import { updateSlotLabelVisibility } from "../utils/calendarUtils";
|
||||||
|
import { createMouseHandlers } from "./mouseHandlers";
|
||||||
|
import { userAttendee } from "../../../features/User/userDataTypes";
|
||||||
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||||
|
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||||
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
|
||||||
|
export interface ViewHandlersProps {
|
||||||
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
|
setSelectedDate: (date: Date) => void;
|
||||||
|
setSelectedMiniDate: (date: Date) => void;
|
||||||
|
onViewChange?: (view: string) => void;
|
||||||
|
calendars: Record<string, Calendars>;
|
||||||
|
tempcalendars: Record<string, Calendars>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||||
|
const {
|
||||||
|
calendarRef,
|
||||||
|
setSelectedDate,
|
||||||
|
setSelectedMiniDate,
|
||||||
|
onViewChange,
|
||||||
|
calendars,
|
||||||
|
tempcalendars,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const handleDayHeaderDidMount = (arg: any) => {
|
||||||
|
if (arg.view.type === "timeGridWeek") {
|
||||||
|
const headerEl = arg.el;
|
||||||
|
|
||||||
|
const handleDayHeaderClick = () => {
|
||||||
|
calendarRef.current?.changeView("timeGridDay", arg.date);
|
||||||
|
setSelectedDate(new Date(arg.date));
|
||||||
|
setSelectedMiniDate(new Date(arg.date));
|
||||||
|
|
||||||
|
if (onViewChange) {
|
||||||
|
onViewChange("timeGridDay");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
headerEl.addEventListener("click", handleDayHeaderClick);
|
||||||
|
(headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDayHeaderWillUnmount = (arg: any) => {
|
||||||
|
const headerEl = arg.el;
|
||||||
|
if ((headerEl as any).__dayHeaderClickHandler) {
|
||||||
|
headerEl.removeEventListener(
|
||||||
|
"click",
|
||||||
|
(headerEl as any).__dayHeaderClickHandler
|
||||||
|
);
|
||||||
|
delete (headerEl as any).__dayHeaderClickHandler;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDidMount = (arg: any) => {
|
||||||
|
const updateNowIndicator = () => {
|
||||||
|
const nowIndicatorArrow = document.querySelector(
|
||||||
|
".fc-timegrid-now-indicator-arrow"
|
||||||
|
) as HTMLElement;
|
||||||
|
if (nowIndicatorArrow) {
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString(undefined, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
nowIndicatorArrow.setAttribute("data-time", timeString);
|
||||||
|
updateSlotLabelVisibility(now);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNowIndicator();
|
||||||
|
const timeInterval = setInterval(updateNowIndicator, 60000);
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element;
|
||||||
|
if (
|
||||||
|
element.classList?.contains("fc-timegrid-now-indicator-arrow") ||
|
||||||
|
element.querySelector?.(".fc-timegrid-now-indicator-arrow") ||
|
||||||
|
element.classList?.contains("fc-timegrid-slot-label") ||
|
||||||
|
element.querySelector?.(".fc-timegrid-slot-label")
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNowIndicator();
|
||||||
|
updateSlotLabelVisibility(new Date());
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
(arg.el as any).__timeInterval = timeInterval;
|
||||||
|
(arg.el as any).__timeObserver = observer;
|
||||||
|
|
||||||
|
if (arg.view.type === "timeGridWeek" || arg.view.type === "timeGridDay") {
|
||||||
|
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||||
|
if (calendarEl) {
|
||||||
|
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||||
|
mouseHandlers.addMouseEventListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewWillUnmount = (arg: any) => {
|
||||||
|
if ((arg.el as any).__timeInterval) {
|
||||||
|
clearInterval((arg.el as any).__timeInterval);
|
||||||
|
delete (arg.el as any).__timeInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((arg.el as any).__timeObserver) {
|
||||||
|
(arg.el as any).__timeObserver.disconnect();
|
||||||
|
delete (arg.el as any).__timeObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||||
|
if (calendarEl) {
|
||||||
|
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||||
|
mouseHandlers.removeMouseEventListeners();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventContent = (arg: any) => {
|
||||||
|
const event = arg.event;
|
||||||
|
if (
|
||||||
|
(!event._def.extendedProps.temp &&
|
||||||
|
!calendars[arg.event._def.extendedProps.calId]) ||
|
||||||
|
(event._def.extendedProps.temp &&
|
||||||
|
!tempcalendars[arg.event._def.extendedProps.calId])
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const attendees = event._def.extendedProps.attendee || [];
|
||||||
|
const isPrivate =
|
||||||
|
event._def.extendedProps.class === "PRIVATE" ||
|
||||||
|
event._def.extendedProps.class === "CONFIDENTIAL";
|
||||||
|
let Icon = null;
|
||||||
|
let titleStyle: React.CSSProperties = {};
|
||||||
|
const ownerEmails = new Set(
|
||||||
|
(event._def.extendedProps.temp ? tempcalendars : calendars)[
|
||||||
|
arg.event._def.extendedProps.calId
|
||||||
|
].ownerEmails?.map((email) => email.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const delegated = (
|
||||||
|
event._def.extendedProps.temp ? tempcalendars : calendars
|
||||||
|
)[arg.event._def.extendedProps.calId].delegated;
|
||||||
|
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||||
|
ownerEmails.has(att.cal_address.toLowerCase())
|
||||||
|
);
|
||||||
|
if (!delegated && showSpecialDisplay.length === 0) return null;
|
||||||
|
|
||||||
|
switch (showSpecialDisplay?.[0]?.partstat) {
|
||||||
|
case "DECLINED":
|
||||||
|
Icon = null;
|
||||||
|
titleStyle.textDecoration = "line-through";
|
||||||
|
break;
|
||||||
|
case "TENTATIVE":
|
||||||
|
Icon = HelpOutlineIcon;
|
||||||
|
break;
|
||||||
|
case "NEEDS-ACTION":
|
||||||
|
Icon = AccessTimeIcon;
|
||||||
|
break;
|
||||||
|
case "ACCEPTED":
|
||||||
|
Icon = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
"div",
|
||||||
|
{ style: { display: "flex", alignItems: "center" } },
|
||||||
|
isPrivate &&
|
||||||
|
React.createElement(LockIcon, {
|
||||||
|
"data-testid": "lock-icon",
|
||||||
|
fontSize: "small",
|
||||||
|
style: { marginRight: "4px" },
|
||||||
|
}),
|
||||||
|
Icon &&
|
||||||
|
React.createElement(Icon, {
|
||||||
|
fontSize: "small",
|
||||||
|
style: { marginRight: "4px" },
|
||||||
|
}),
|
||||||
|
React.createElement("span", { style: titleStyle }, event.title)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventDidMount = (arg: any) => {
|
||||||
|
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||||
|
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||||
|
const ownerEmails = new Set(
|
||||||
|
calendars[arg.event._def.extendedProps.calId].ownerEmails?.map((email) =>
|
||||||
|
email.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||||
|
ownerEmails.has(att.cal_address.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showSpecialDisplay[0]) return;
|
||||||
|
|
||||||
|
arg.el.classList.remove(
|
||||||
|
"declined-event",
|
||||||
|
"tentative-event",
|
||||||
|
"needs-action-event"
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (showSpecialDisplay[0].partstat) {
|
||||||
|
case "DECLINED":
|
||||||
|
arg.el.classList.add("declined-event");
|
||||||
|
break;
|
||||||
|
case "TENTATIVE":
|
||||||
|
arg.el.classList.add("tentative-event");
|
||||||
|
break;
|
||||||
|
case "NEEDS-ACTION":
|
||||||
|
arg.el.classList.add("needs-action-event");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDayHeaderDidMount,
|
||||||
|
handleDayHeaderWillUnmount,
|
||||||
|
handleViewDidMount,
|
||||||
|
handleViewWillUnmount,
|
||||||
|
handleEventContent,
|
||||||
|
handleEventDidMount,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||||
|
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||||
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
|
||||||
|
import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice";
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
|
||||||
|
let timeDiff = Math.abs(currentMinutes - labelMinutes);
|
||||||
|
|
||||||
|
if (timeDiff > 12 * 60) {
|
||||||
|
timeDiff = 24 * 60 - timeDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeDiff <= 15) {
|
||||||
|
labelElement.style.opacity = "0.2";
|
||||||
|
} else {
|
||||||
|
labelElement.style.opacity = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const eventToFullCalendarFormat = (
|
||||||
|
filteredEvents: CalendarEvent[],
|
||||||
|
filteredTempEvents: CalendarEvent[],
|
||||||
|
userId: string | undefined
|
||||||
|
) => {
|
||||||
|
return filteredEvents
|
||||||
|
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||||
|
.map((e) => {
|
||||||
|
if (e.calId.split("/")[0] === userId) {
|
||||||
|
return { ...e, editable: true };
|
||||||
|
}
|
||||||
|
return { ...e, editable: false };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractEvents = (
|
||||||
|
selectedCalendars: string[],
|
||||||
|
calendars: Record<string, Calendars>
|
||||||
|
) => {
|
||||||
|
let filteredEvents: CalendarEvent[] = [];
|
||||||
|
selectedCalendars.forEach((id) => {
|
||||||
|
if (calendars[id].events) {
|
||||||
|
filteredEvents = filteredEvents
|
||||||
|
.concat(
|
||||||
|
Object.keys(calendars[id].events).map(
|
||||||
|
(eventid) => calendars[id].events[eventid]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((event) => !(event.status === "CANCELLED"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filteredEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCalsDetails = (
|
||||||
|
selectedCalendars: string[],
|
||||||
|
pending: boolean,
|
||||||
|
calendars: Record<string, Calendars>,
|
||||||
|
rangeKey: string,
|
||||||
|
dispatch: Function,
|
||||||
|
calendarRange: { start: Date; end: Date },
|
||||||
|
calType?: "temp"
|
||||||
|
) => {
|
||||||
|
selectedCalendars.forEach((id) => {
|
||||||
|
if (Object.keys(calendars[id].events).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pending && rangeKey) {
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId: id,
|
||||||
|
match: {
|
||||||
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
calType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user