From e6bb665c87717ca2191a721def8e28637e6c9f90 Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Mon, 22 Sep 2025 15:56:50 +0700 Subject: [PATCH] add font Inter update UI cho main grid calendar. add hover css and state. click able on header cell --- public/index.html | 6 +- src/components/Calendar/Calendar.css | 13 -- src/components/Calendar/Calendar.tsx | 159 ++++++++++++++++++++- src/components/Calendar/CustomCalendar.css | 111 +++++++++++++- 4 files changed, 265 insertions(+), 24 deletions(-) diff --git a/public/index.html b/public/index.html index 90cace4..65fbf91 100644 --- a/public/index.html +++ b/public/index.html @@ -26,7 +26,11 @@ href="https://fonts.googleapis.com/css2?family=Cal+Sans&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" /> - Twake Calendar + + React App diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index eed1f96..22c0ded 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -15,7 +15,6 @@ flex-grow: 1; height: 100%; overflow: hidden; - padding: 0 25px 0 10px; } .calendarListHeader { @@ -42,18 +41,6 @@ border: dashed 1px #ffffff; } -.fc .fc-timegrid-slot { - height: auto; - min-height: 54px !important; -} - -.fc .fc-timegrid-slot-label { - display: flex; - align-items: flex-start; - padding-top: 2px; - height: 100%; -} - .fc-event-main span, .fc-daygrid-event div { display: block !important; diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 5ec75ee..794d436 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -320,7 +320,7 @@ export default function CalendarApp() { }} plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} initialView="timeGridWeek" - firstDay={1} + firstDay={0} editable={true} selectable={true} timeZone="local" @@ -361,8 +361,8 @@ export default function CalendarApp() { )} weekNumbers weekNumberFormat={{ week: "long" }} - slotDuration={"00:30:00"} - slotLabelInterval={"00:30:00"} + slotDuration={"01:00:00"} + slotLabelInterval={"01:00:00"} scrollTime={new Date(Date.now() - 2 * 60 * 60 * 1000) .toTimeString() .slice(0, 5)} @@ -402,6 +402,159 @@ export default function CalendarApp() { ); }} + dayHeaderDidMount={(arg) => { + // Add click handler to day headers in week view + if (arg.view.type === "timeGridWeek") { + const headerEl = arg.el; + + const handleDayHeaderClick = () => { + // Switch to day view and navigate to the clicked date + calendarRef.current?.changeView("timeGridDay", arg.date); + setSelectedDate(new Date(arg.date)); + setSelectedMiniDate(new Date(arg.date)); + }; + + 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) => { + // 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 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 diff --git a/src/components/Calendar/CustomCalendar.css b/src/components/Calendar/CustomCalendar.css index d892e1c..c950534 100644 --- a/src/components/Calendar/CustomCalendar.css +++ b/src/components/Calendar/CustomCalendar.css @@ -5,8 +5,11 @@ .fc-theme-standard td, .fc-theme-standard th { border: 1px solid #b8c1cc; +} +th.fc-col-header-cell.fc-day { border-left: 0; } + a.fc-timegrid-axis-cushion { color: #243b55; text-align: center; @@ -24,11 +27,12 @@ a.fc-timegrid-axis-cushion { font-weight: 500; line-height: 20px; letter-spacing: 0.25px; + font-family: "Inter", sans-serif; } .fc-daygrid-day-top .fc-daygrid-day-number, span.fc-daygrid-day-number { color: #243b55; - margin: 0 8px 0 0; + margin: 0 6px 0 0; font-family: Roboto; font-size: 28px; font-style: normal; @@ -44,11 +48,12 @@ span.fc-daygrid-day-number:after { content: ""; z-index: -1; border-radius: 50%; - width: 48px; - height: 48px; + width: 45px; + height: 45px; position: absolute; - left: 0; - top: -1px; + top: 1px; + left: 1px; + transition: background-color 0.3s ease; } span.fc-daygrid-day-number.current-date { color: #fff; @@ -65,10 +70,16 @@ th.fc-timegrid-axis.fc-scrollgrid-shrink { border-right: 1px solid #b8c1cc; } .fc .fc-timegrid-axis-cushion { - min-width: 70px; - max-width: 70px; + min-width: 80px; + max-width: 80px; padding: 0; text-align: left; + font-family: Roboto; + font-size: 12.507px; + font-style: normal; + font-weight: 400; + line-height: 18.76px; + padding: 0 0 0 10px; } .fc .fc-timegrid-divider { display: none; @@ -85,6 +96,7 @@ th.fc-timegrid-axis.fc-scrollgrid-shrink { } th.fc-col-header-cell.fc-day { border-right: 0; + border-bottom: 0; } .fc .fc-scrollgrid { border: 0; @@ -106,3 +118,88 @@ th.fc-col-header-cell.fc-day { top: 9.5px; right: -15px; } +.fc .fc-timegrid-col.fc-day-today, +.fc .fc-daygrid-day.fc-day-today { + background-color: #fff; +} +.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events { + min-height: 36px; +} +.fc .fc-daygrid-body-natural .fc-daygrid-day-events { + margin-bottom: 0; +} +.fc .fc-timegrid-now-indicator-line { + z-index: 5; + border: 0 solid #f67e35; + border-top-width: 2px; + position: absolute; + left: 0; + right: 0; +} +.fc .fc-timegrid-now-indicator-line::after { + content: ""; + z-index: 5; + background-color: #f67e35; + border-radius: 50%; + width: 10px; + height: 10px; + position: absolute; + top: -6px; + left: -5px; +} +.fc .fc-timegrid-now-indicator-arrow { + display: none; +} +.fc-day-today .fc-timegrid-now-indicator-container { + overflow: unset; +} +.fc .fc-timegrid-slot { + height: auto; + min-height: 40px !important; +} +.fc .fc-timegrid-slot-label { + display: flex; + align-items: flex-start; + padding-top: 2px; + height: 100%; +} +tr:has(> td.fc-timegrid-divider.fc-cell-shaded) { + display: none; +} +.fc .fc-scrollgrid-section td { + border-bottom: 0; +} +.fc .fc-scrollgrid-section td .fc-daygrid-day-frame { + border-bottom: 1px solid #b8c1cc; +} +th.fc-col-header-cell.fc-day { + cursor: pointer; +} +th.fc-col-header-cell.fc-day:hover + .fc-daygrid-day-number:not(.current-date)::after { + background-color: #b8c1cc56; +} + +/* Hover effects for month view using CSS */ +.fc-daygrid-day:hover { + background-color: #b8c1cc1a !important; + transition: background-color 0.3s ease; +} + +/* Hover effects for week and day views using JavaScript */ +.hoverable-day-cell { + transition: background-color 0.3s ease; +} + +.hour-highlight { + transition: background-color 0.3s ease; + background-color: #b8c1cc1a !important; + position: absolute; + left: 0; + right: 0; + pointer-events: none; + z-index: 1; +} +.fc-timegrid-slot-label { + background-color: #fff !important; +}