feat: move headerToolbar functionality to Menubar component

- Remove headerToolbar from FullCalendar and set to false
- Create CalendarLayout component to manage state between Menubar and Calendar
- Add navigation controls (prev/today/next) to Menubar left-menu using MUI ButtonGroup
- Add current date/time display (April 2024 format) to Menubar left-menu
- Add view selector (Month/Week/Day) to Menubar right-menu using MUI Select
- Implement real-time date synchronization between Calendar and Menubar
This commit is contained in:
lenhanphung
2025-09-24 12:23:35 +07:00
parent 5f02f77769
commit 12dc0d7fe1
5 changed files with 229 additions and 86 deletions
+2 -11
View File
@@ -1,13 +1,12 @@
import { Suspense } from "react";
import { Route, Routes } from "react-router-dom";
import { HistoryRouter as Router } from "redux-first-history/rr6";
import { Menubar } from "./components/Menubar/Menubar";
import { CallbackResume } from "./features/User/LoginCallback";
import { history } from "./app/store";
import "./App.css";
import { Loading } from "./components/Loading/Loading";
import HandleLogin from "./features/User/HandleLogin";
import CalendarApp from "./components/Calendar/Calendar";
import CalendarLayout from "./components/Calendar/CalendarLayout";
import { Error } from "./components/Error/Error";
function App() {
return (
@@ -15,15 +14,7 @@ function App() {
<Router history={history}>
<Routes>
<Route path="/" element={<HandleLogin />} />
<Route
path="/calendar"
element={
<div className="App">
<Menubar />
<CalendarApp />
</div>
}
/>
<Route path="/calendar" element={<CalendarLayout />} />
<Route path="/callback" element={<CallbackResume />} />
<Route path="/error" element={<Error />} />
</Routes>
+31 -46
View File
@@ -7,15 +7,13 @@ import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import ReactCalendar from "react-calendar";
import "./Calendar.css";
import "./CustomCalendar.css";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import EventPopover from "../../features/Events/EventModal";
import CalendarPopover from "../../features/Calendars/CalendarModal";
import { CalendarEvent } from "../../features/Events/EventsTypes";
import CalendarSelection from "./CalendarSelection";
import {
getCalendarDetailAsync,
getCalendarsListAsync,
getEventAsync,
putEventAsync,
updateEventLocal,
@@ -40,12 +38,15 @@ import { userAttendee } from "../../features/User/userDataTypes";
import { TempCalendarsInput } from "./TempCalendarsInput";
import Button from "@mui/material/Button";
export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null);
interface CalendarAppProps {
calendarRef: React.RefObject<CalendarApi | null>;
onDateChange?: (date: Date) => void;
}
export default function CalendarApp({ calendarRef, onDateChange }: CalendarAppProps) {
const [selectedDate, setSelectedDate] = useState(new Date());
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
const tokens = useAppSelector((state) => state.user.tokens);
const user = useAppSelector((state) => state.user.userData);
const dispatch = useAppDispatch();
if (!tokens) {
@@ -115,15 +116,20 @@ export default function CalendarApp() {
);
useEffect(() => {
updateCalsDetails(
selectedCalendars,
pending,
calendars,
rangeKey,
dispatch,
calendarRange
);
}, [rangeKey, selectedCalendars]);
selectedCalendars.forEach((id) => {
if (!pending && rangeKey) {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
);
}
});
}, [rangeKey, selectedCalendars, pending, dispatch, calendarRange.start, calendarRange.end]);
useEffect(() => {
updateCalsDetails(
@@ -327,29 +333,7 @@ export default function CalendarApp() {
height={"100%"}
select={handleDateSelect}
nowIndicator
customButtons={{
refresh: {
text: "↻",
click: async () => {
await dispatch(getCalendarsListAsync());
selectedCalendars.forEach((id) => {
if (!pending && rangeKey) {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(
calendarRange.start
),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
);
}
});
},
},
}}
headerToolbar={false}
views={{
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
}}
@@ -372,15 +356,21 @@ export default function CalendarApp() {
hour12: false,
}}
datesSet={(arg) => {
// Get the current date from calendar API to ensure consistency
const calendarCurrentDate = calendarRef.current?.getDate() || new Date(arg.start);
if (arg.view.type === "dayGridMonth") {
setSelectedDate(new Date(arg.start));
const midTimestamp =
(arg.start.getTime() + arg.end.getTime()) / 2;
setSelectedMiniDate(new Date(midTimestamp));
setSelectedMiniDate(calendarCurrentDate);
} else {
setSelectedDate(new Date(arg.start));
setSelectedMiniDate(new Date(arg.start));
}
// Always use the calendar's current date for consistency
if (onDateChange) {
onDateChange(calendarCurrentDate);
}
}}
dayHeaderContent={(arg) => {
const date = arg.date.getDate();
@@ -695,11 +685,6 @@ export default function CalendarApp() {
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
);
}}
headerToolbar={{
left: "title",
center: "prev,today,next",
right: "refresh,dayGridMonth,timeGridWeek,timeGridDay",
}}
eventContent={(arg) => {
const event = arg.event;
if (
@@ -0,0 +1,64 @@
import React, { useRef, useState } from "react";
import { Menubar, MenubarProps } from "../Menubar/Menubar";
import CalendarApp from "./Calendar";
import { useAppDispatch } from "../../app/hooks";
import {
getCalendarDetailAsync,
getCalendarsListAsync,
} from "../../features/Calendars/CalendarSlice";
import {
formatDateToYYYYMMDDTHHMMSS,
getCalendarRange,
} from "../../utils/dateUtils";
import { useAppSelector } from "../../app/hooks";
export default function CalendarLayout() {
const calendarRef = useRef<any>(null);
const dispatch = useAppDispatch();
const selectedCalendars = useAppSelector((state) => state.calendars.list);
const userId = useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const handleRefresh = async () => {
await dispatch(getCalendarsListAsync());
// Get current calendar range
if (calendarRef.current) {
const view = calendarRef.current.getApi().view;
const calendarRange = getCalendarRange(view.activeStart);
// Refresh events for selected calendars
Object.keys(selectedCalendars).forEach((id) => {
if (id.split("/")[0] === userId) {
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
);
}
});
}
};
const handleDateChange = (date: Date) => {
setCurrentDate(date);
};
const menubarProps: MenubarProps = {
calendarRef,
onRefresh: handleRefresh,
currentDate,
onDateChange: handleDateChange,
};
return (
<div className="App">
<Menubar {...menubarProps} />
<CalendarApp calendarRef={calendarRef} onDateChange={handleDateChange} />
</div>
);
}
+5
View File
@@ -93,3 +93,8 @@
font-size: 14px;
text-align: center;
}
.left-menu {
display: flex;
align-items: center;
}
+127 -29
View File
@@ -1,11 +1,25 @@
import React, { useState } from "react";
import logo from "../../static/images/calendar.svg";
import AppsIcon from "@mui/icons-material/Apps";
import RefreshIcon from "@mui/icons-material/Refresh";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import "./Menubar.css";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { stringToColor } from "../../features/Events/EventDisplay";
import { Avatar, IconButton, Popover } from "@mui/material";
import {
Avatar,
IconButton,
Popover,
ButtonGroup,
Button,
Select,
MenuItem,
FormControl,
Typography
} from "@mui/material";
import { push } from "redux-first-history";
import { CalendarApi } from "@fullcalendar/core";
export type AppIconProps = {
name: string;
@@ -13,10 +27,18 @@ export type AppIconProps = {
icon: string;
};
export function Menubar() {
export type MenubarProps = {
calendarRef: React.RefObject<CalendarApi | null>;
onRefresh: () => void;
currentDate: Date;
onDateChange?: (date: Date) => void;
};
export function Menubar({ calendarRef, onRefresh, currentDate, onDateChange }: MenubarProps) {
const user = useAppSelector((state) => state.user.userData);
const applist: AppIconProps[] = (window as any).appList ?? [];
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [currentView, setCurrentView] = useState<string>("timeGridWeek");
const dispatch = useAppDispatch();
if (!user) {
@@ -30,37 +52,112 @@ export function Menubar() {
setAnchorEl(null);
};
const handleNavigation = (action: 'prev' | 'next' | 'today') => {
if (!calendarRef.current) return;
switch (action) {
case 'prev':
calendarRef.current.prev();
break;
case 'next':
calendarRef.current.next();
break;
case 'today':
calendarRef.current.today();
break;
}
// Notify parent about date change after navigation
if (onDateChange) {
const newDate = calendarRef.current.getDate();
onDateChange(newDate);
}
};
const handleViewChange = (view: string) => {
if (!calendarRef.current) return;
setCurrentView(view);
calendarRef.current.changeView(view);
// Notify parent about date change after view change
if (onDateChange) {
const newDate = calendarRef.current.getDate();
onDateChange(newDate);
}
};
const handleRefresh = () => {
onRefresh();
};
const open = Boolean(anchorEl);
return (
<>
<header className="menubar">
<MainTitle />
<div className="menubar-item search-bar">
<p>big search bar</p>
</div>
<div className="left-menu">
<MainTitle />
<div className="navigation-controls">
<ButtonGroup variant="outlined" size="small">
<Button onClick={() => handleNavigation('prev')}>
<ChevronLeftIcon />
</Button>
<Button onClick={() => handleNavigation('today')}>
Today
</Button>
<Button onClick={() => handleNavigation('next')}>
<ChevronRightIcon />
</Button>
</ButtonGroup>
</div>
<div className="menubar-item">
{applist.length > 0 && (
<IconButton onClick={handleOpen}>
<AppsIcon />
</IconButton>
)}
<Avatar
sx={{
bgcolor: stringToColor(
user && user.family_name
? user.family_name
: user && user.email
? user.email
: ""
),
}}
sizes="large"
>
{user?.name && user?.family_name
? `${user.name[0]}${user.family_name[0]}`
: (user?.email?.[0] ?? "")}
</Avatar>
<div className="current-date-time">
<Typography variant="h6" component="div">
{currentDate.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
})}
</Typography>
</div>
</div>
<div className="right-menu">
<div className="menubar-item">
<FormControl size="small" sx={{ minWidth: 120, mr: 2 }}>
<Select
value={currentView}
onChange={(e) => handleViewChange(e.target.value)}
variant="outlined"
>
<MenuItem value="dayGridMonth">Month</MenuItem>
<MenuItem value="timeGridWeek">Week</MenuItem>
<MenuItem value="timeGridDay">Day</MenuItem>
</Select>
</FormControl>
{applist.length > 0 && (
<IconButton onClick={handleOpen} sx={{ mr: 1 }}>
<AppsIcon />
</IconButton>
)}
<Avatar
sx={{
bgcolor: stringToColor(
user && user.family_name
? user.family_name
: user && user.email
? user.email
: ""
),
}}
sizes="large"
>
{user?.name && user?.family_name
? `${user.name[0]}${user.family_name[0]}`
: (user?.email?.[0] ?? "")}
</Avatar>
</div>
</div>
</header>
<Popover
@@ -88,7 +185,7 @@ export function Menubar() {
export function MainTitle() {
return (
<div className="menubar-item tc-home">
<img className="logo" src={logo} />
<img className="logo" src={logo} alt="Calendar" />
<p>
<span className="twake">Twake</span>
<span className="calendar-text">Calendar</span>
@@ -102,6 +199,7 @@ function AppIcon({ prop }: { prop: AppIconProps }) {
<a
href={prop.link}
target="_blank"
rel="noreferrer"
style={{ textDecoration: "none", color: "inherit" }}
>
<div>