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
This commit is contained in:
lenhanphung
2025-09-25 14:57:04 +07:00
parent 24efa2b62e
commit b9d7a8fa6b
5 changed files with 72 additions and 17 deletions
+60 -8
View File
@@ -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<CalendarApi | null>;
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(
@@ -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;
+2 -2
View File
@@ -15,9 +15,9 @@ export default function EventDuplication({
const [openModal, setOpenModal] = useState(false);
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>({
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<CalendarApi | null>(null);
+2 -4
View File
@@ -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
+3 -3
View File
@@ -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;