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:
+2
-11
@@ -1,13 +1,12 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
||||||
import { Menubar } from "./components/Menubar/Menubar";
|
|
||||||
import { CallbackResume } from "./features/User/LoginCallback";
|
import { CallbackResume } from "./features/User/LoginCallback";
|
||||||
import { history } from "./app/store";
|
import { history } from "./app/store";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Loading } from "./components/Loading/Loading";
|
import { Loading } from "./components/Loading/Loading";
|
||||||
import HandleLogin from "./features/User/HandleLogin";
|
import HandleLogin from "./features/User/HandleLogin";
|
||||||
import CalendarApp from "./components/Calendar/Calendar";
|
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||||
import { Error } from "./components/Error/Error";
|
import { Error } from "./components/Error/Error";
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -15,15 +14,7 @@ function App() {
|
|||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HandleLogin />} />
|
<Route path="/" element={<HandleLogin />} />
|
||||||
<Route
|
<Route path="/calendar" element={<CalendarLayout />} />
|
||||||
path="/calendar"
|
|
||||||
element={
|
|
||||||
<div className="App">
|
|
||||||
<Menubar />
|
|
||||||
<CalendarApp />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/callback" element={<CallbackResume />} />
|
<Route path="/callback" element={<CallbackResume />} />
|
||||||
<Route path="/error" element={<Error />} />
|
<Route path="/error" element={<Error />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
|||||||
import ReactCalendar from "react-calendar";
|
import ReactCalendar from "react-calendar";
|
||||||
import "./Calendar.css";
|
import "./Calendar.css";
|
||||||
import "./CustomCalendar.css";
|
import "./CustomCalendar.css";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import EventPopover from "../../features/Events/EventModal";
|
import EventPopover from "../../features/Events/EventModal";
|
||||||
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
import CalendarSelection from "./CalendarSelection";
|
import CalendarSelection from "./CalendarSelection";
|
||||||
import {
|
import {
|
||||||
getCalendarDetailAsync,
|
getCalendarDetailAsync,
|
||||||
getCalendarsListAsync,
|
|
||||||
getEventAsync,
|
getEventAsync,
|
||||||
putEventAsync,
|
putEventAsync,
|
||||||
updateEventLocal,
|
updateEventLocal,
|
||||||
@@ -40,12 +38,15 @@ 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";
|
||||||
|
|
||||||
export default function CalendarApp() {
|
interface CalendarAppProps {
|
||||||
const calendarRef = useRef<CalendarApi | null>(null);
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
|
onDateChange?: (date: Date) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarApp({ calendarRef, onDateChange }: CalendarAppProps) {
|
||||||
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 user = useAppSelector((state) => state.user.userData);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
if (!tokens) {
|
if (!tokens) {
|
||||||
@@ -115,15 +116,20 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCalsDetails(
|
selectedCalendars.forEach((id) => {
|
||||||
selectedCalendars,
|
if (!pending && rangeKey) {
|
||||||
pending,
|
dispatch(
|
||||||
calendars,
|
getCalendarDetailAsync({
|
||||||
rangeKey,
|
calId: id,
|
||||||
dispatch,
|
match: {
|
||||||
calendarRange
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
);
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
}, [rangeKey, selectedCalendars]);
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [rangeKey, selectedCalendars, pending, dispatch, calendarRange.start, calendarRange.end]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCalsDetails(
|
updateCalsDetails(
|
||||||
@@ -327,29 +333,7 @@ export default function CalendarApp() {
|
|||||||
height={"100%"}
|
height={"100%"}
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
nowIndicator
|
nowIndicator
|
||||||
customButtons={{
|
headerToolbar={false}
|
||||||
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),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
views={{
|
views={{
|
||||||
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
||||||
}}
|
}}
|
||||||
@@ -372,15 +356,21 @@ export default function CalendarApp() {
|
|||||||
hour12: false,
|
hour12: false,
|
||||||
}}
|
}}
|
||||||
datesSet={(arg) => {
|
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") {
|
if (arg.view.type === "dayGridMonth") {
|
||||||
setSelectedDate(new Date(arg.start));
|
setSelectedDate(new Date(arg.start));
|
||||||
const midTimestamp =
|
setSelectedMiniDate(calendarCurrentDate);
|
||||||
(arg.start.getTime() + arg.end.getTime()) / 2;
|
|
||||||
setSelectedMiniDate(new Date(midTimestamp));
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedDate(new Date(arg.start));
|
setSelectedDate(new Date(arg.start));
|
||||||
setSelectedMiniDate(new Date(arg.start));
|
setSelectedMiniDate(new Date(arg.start));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always use the calendar's current date for consistency
|
||||||
|
if (onDateChange) {
|
||||||
|
onDateChange(calendarCurrentDate);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
dayHeaderContent={(arg) => {
|
dayHeaderContent={(arg) => {
|
||||||
const date = arg.date.getDate();
|
const date = arg.date.getDate();
|
||||||
@@ -695,11 +685,6 @@ export default function CalendarApp() {
|
|||||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
headerToolbar={{
|
|
||||||
left: "title",
|
|
||||||
center: "prev,today,next",
|
|
||||||
right: "refresh,dayGridMonth,timeGridWeek,timeGridDay",
|
|
||||||
}}
|
|
||||||
eventContent={(arg) => {
|
eventContent={(arg) => {
|
||||||
const event = arg.event;
|
const event = arg.event;
|
||||||
if (
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -93,3 +93,8 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import logo from "../../static/images/calendar.svg";
|
import logo from "../../static/images/calendar.svg";
|
||||||
import AppsIcon from "@mui/icons-material/Apps";
|
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 "./Menubar.css";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { stringToColor } from "../../features/Events/EventDisplay";
|
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 { push } from "redux-first-history";
|
||||||
|
import { CalendarApi } from "@fullcalendar/core";
|
||||||
|
|
||||||
export type AppIconProps = {
|
export type AppIconProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,10 +27,18 @@ export type AppIconProps = {
|
|||||||
icon: string;
|
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 user = useAppSelector((state) => state.user.userData);
|
||||||
const applist: AppIconProps[] = (window as any).appList ?? [];
|
const applist: AppIconProps[] = (window as any).appList ?? [];
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [currentView, setCurrentView] = useState<string>("timeGridWeek");
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -30,37 +52,112 @@ export function Menubar() {
|
|||||||
setAnchorEl(null);
|
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);
|
const open = Boolean(anchorEl);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="menubar">
|
<header className="menubar">
|
||||||
<MainTitle />
|
<div className="left-menu">
|
||||||
<div className="menubar-item search-bar">
|
<MainTitle />
|
||||||
<p>big search bar</p>
|
|
||||||
</div>
|
<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">
|
<div className="current-date-time">
|
||||||
{applist.length > 0 && (
|
<Typography variant="h6" component="div">
|
||||||
<IconButton onClick={handleOpen}>
|
{currentDate.toLocaleDateString('en-US', {
|
||||||
<AppsIcon />
|
month: 'long',
|
||||||
</IconButton>
|
year: 'numeric'
|
||||||
)}
|
})}
|
||||||
<Avatar
|
</Typography>
|
||||||
sx={{
|
</div>
|
||||||
bgcolor: stringToColor(
|
</div>
|
||||||
user && user.family_name
|
<div className="right-menu">
|
||||||
? user.family_name
|
<div className="menubar-item">
|
||||||
: user && user.email
|
<FormControl size="small" sx={{ minWidth: 120, mr: 2 }}>
|
||||||
? user.email
|
<Select
|
||||||
: ""
|
value={currentView}
|
||||||
),
|
onChange={(e) => handleViewChange(e.target.value)}
|
||||||
}}
|
variant="outlined"
|
||||||
sizes="large"
|
>
|
||||||
>
|
<MenuItem value="dayGridMonth">Month</MenuItem>
|
||||||
{user?.name && user?.family_name
|
<MenuItem value="timeGridWeek">Week</MenuItem>
|
||||||
? `${user.name[0]}${user.family_name[0]}`
|
<MenuItem value="timeGridDay">Day</MenuItem>
|
||||||
: (user?.email?.[0] ?? "")}
|
</Select>
|
||||||
</Avatar>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -88,7 +185,7 @@ export function Menubar() {
|
|||||||
export function MainTitle() {
|
export function MainTitle() {
|
||||||
return (
|
return (
|
||||||
<div className="menubar-item tc-home">
|
<div className="menubar-item tc-home">
|
||||||
<img className="logo" src={logo} />
|
<img className="logo" src={logo} alt="Calendar" />
|
||||||
<p>
|
<p>
|
||||||
<span className="twake">Twake</span>
|
<span className="twake">Twake</span>
|
||||||
<span className="calendar-text">Calendar</span>
|
<span className="calendar-text">Calendar</span>
|
||||||
@@ -102,6 +199,7 @@ function AppIcon({ prop }: { prop: AppIconProps }) {
|
|||||||
<a
|
<a
|
||||||
href={prop.link}
|
href={prop.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
style={{ textDecoration: "none", color: "inherit" }}
|
style={{ textDecoration: "none", color: "inherit" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user