Merge pull request #130 from linagora/UI/style-main-grid

UI/style main grid
This commit is contained in:
lenhanphung
2025-09-25 17:57:25 +07:00
committed by GitHub
17 changed files with 1094 additions and 476 deletions
+19 -22
View File
@@ -3,6 +3,7 @@ import CalendarApp from "../../src/components/Calendar/Calendar";
import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
import { renderWithProviders } from "../utils/Renderwithproviders";
import { searchUsers } from "../../src/features/User/userAPI";
import { useRef } from "react";
import userEvent from "@testing-library/user-event";
jest.mock("../../src/features/User/userAPI");
@@ -10,6 +11,12 @@ const mockedSearchUsers = searchUsers as jest.MockedFunction<
typeof searchUsers
>;
// Test wrapper component to provide calendarRef
function CalendarTestWrapper() {
const calendarRef = useRef(null);
return <CalendarApp calendarRef={calendarRef} />;
}
describe("CalendarSelection", () => {
const today = new Date();
const start = new Date(today);
@@ -127,7 +134,11 @@ describe("CalendarSelection", () => {
},
};
it("renders calendars", async () => {
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
@@ -136,26 +147,12 @@ describe("CalendarSelection", () => {
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
});
it("refresh calendars", async () => {
const spy = jest
.spyOn(eventThunks, "getCalendarDetailAsync")
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
renderWithProviders(<CalendarApp />, preloadedState);
const refreshButton = screen.getByRole("button", {
name: "↻",
});
fireEvent.click(refreshButton);
await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
});
it("open accordeon when clicking on button only", () => {
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
@@ -218,7 +215,7 @@ describe("calendar Availability search", () => {
},
]);
renderWithProviders(<CalendarApp />, preloadedState);
renderWithProviders(<CalendarTestWrapper />, preloadedState);
const input = screen.getByPlaceholderText(/search user/i);
userEvent.type(input, "New");
@@ -243,7 +240,7 @@ describe("calendar Availability search", () => {
.mockImplementation((payload) => {
return () => Promise.resolve(payload) as any;
});
renderWithProviders(<CalendarApp />, preloadedState);
renderWithProviders(<CalendarTestWrapper />, preloadedState);
const input = screen.getByPlaceholderText(/search user/i);
userEvent.type(input, "Alice");
@@ -69,7 +69,11 @@ describe("CalendarApp integration", () => {
},
};
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
};
it("renders the event on the calendar and calendarRef works", async () => {
@@ -157,7 +161,11 @@ describe("CalendarApp integration", () => {
class: "PRIVATE",
title: "Private Event",
});
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
const eventEls = screen.getAllByText("Private Event");
const found = eventEls.some((eventEl) =>
@@ -171,7 +179,11 @@ describe("CalendarApp integration", () => {
class: "CONFIDENTIAL",
title: "Confidential Event",
});
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
const eventEls = screen.getAllByText("Confidential Event");
const found = eventEls.some((eventEl) =>
@@ -185,7 +197,11 @@ describe("CalendarApp integration", () => {
class: "PUBLIC",
title: "Public Event",
});
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
const eventEls = screen.getAllByText("Public Event");
const found = eventEls.some((eventEl) =>
+193 -23
View File
@@ -21,20 +21,55 @@ describe("Calendar App Component Display Tests", () => {
{ name: "Twake", link: "/twake", icon: "twake.svg" },
{ name: "Calendar", link: "/calendar", icon: "calendar.svg" },
];
renderWithProviders(<Menubar />, preloadedState);
const navbarElement = screen.getByText("Twake");
expect(navbarElement).toBeInTheDocument();
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const logoElement = screen.getByAltText("Calendar");
expect(logoElement).toBeInTheDocument();
});
it("renders the main title", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
renderWithProviders(<Menubar />, preloadedState);
expect(screen.getByText(/Twake/i)).toBeInTheDocument();
expect(screen.getByText(/Calendar/i)).toBeInTheDocument();
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByAltText("Calendar")).toBeInTheDocument();
});
it("shows avatar with user initials", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("JD")).toBeInTheDocument();
});
@@ -51,7 +86,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("t")).toBeInTheDocument();
});
@@ -70,7 +117,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("t")).toBeInTheDocument();
});
@@ -88,7 +147,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("t")).toBeInTheDocument();
});
@@ -106,7 +177,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("JD")).toBeInTheDocument();
});
@@ -124,7 +207,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByText("t")).toBeInTheDocument();
});
@@ -142,7 +237,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
// Should not crash and show empty string
const avatar = screen.getByRole("img", { hidden: true });
expect(avatar).toBeInTheDocument();
@@ -162,7 +269,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
// Should not crash and show empty string
const avatar = screen.getByRole("img", { hidden: true });
expect(avatar).toBeInTheDocument();
@@ -182,7 +301,19 @@ describe("Calendar App Component Display Tests", () => {
},
},
};
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
// Should not crash and show empty string
const avatar = screen.getByRole("img", { hidden: true });
expect(avatar).toBeInTheDocument();
@@ -190,14 +321,41 @@ describe("Calendar App Component Display Tests", () => {
it("shows AppsIcon when applist is not empty", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
renderWithProviders(<Menubar />, preloadedState);
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
expect(screen.getByTestId("AppsIcon")).toBeInTheDocument();
});
it("opens popover when clicking AppsIcon", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
renderWithProviders(<Menubar />, preloadedState);
const appsButton = screen.getByRole("button");
(window as any).appList = [
{ name: "Twake", icon: "twake.svg", link: "/twake" },
{ name: "Calendar", icon: "calendar.svg", link: "/calendar" },
];
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const appsButton = screen.getByTestId("AppsIcon");
fireEvent.click(appsButton);
expect(screen.getByText("Twake")).toBeInTheDocument();
expect(screen.getByText("Calendar")).toBeInTheDocument();
@@ -205,11 +363,23 @@ describe("Calendar App Component Display Tests", () => {
it("renders app icons as links", () => {
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
renderWithProviders(<Menubar />, preloadedState);
const appsButton = screen.getByRole("button");
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const appsButton = screen.getByTestId("AppsIcon");
fireEvent.click(appsButton);
const twakeLink = screen.getByRole("link", { name: /test/i });
expect(twakeLink).toHaveAttribute("href", "test");
const testLink = screen.getByRole("link", { name: /test/i });
expect(testLink).toHaveAttribute("href", "test");
});
});
+6 -191
View File
@@ -1,5 +1,5 @@
import { renderWithProviders } from "../utils/Renderwithproviders";
import { fireEvent, screen } from "@testing-library/react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { jest } from "@jest/globals";
import CalendarApp from "../../src/components/Calendar/Calendar";
import * as appHooks from "../../src/app/hooks";
@@ -44,7 +44,11 @@ describe("MiniCalendar", () => {
pending: false,
},
};
renderWithProviders(<CalendarApp />, preloadedState);
const mockCalendarRef = { current: null };
renderWithProviders(
<CalendarApp calendarRef={mockCalendarRef} />,
preloadedState
);
};
it("renders mini calendar with today in orange", async () => {
@@ -55,193 +59,4 @@ describe("MiniCalendar", () => {
const todayTile = screen.getByTestId(dateTestId);
expect(todayTile?.parentElement).toHaveClass("today");
});
it.each([
{ today: new Date(2025, 8, 1), label: "Monday 1 Sept 2025" },
{ today: new Date(2025, 8, 3), label: "Wednesday 3 Sept 2025" },
{ today: new Date(2025, 8, 7), label: "Sunday 7 Sept 2025" },
])(
"renders mini calendar with the week in gray (except for today) when today is $label",
({ today }) => {
jest.useFakeTimers();
jest.setSystemTime(today);
renderCalendar();
const monday = new Date(2025, 8, 1); // Monday Sept 1
for (let i = 0; i < 7; i++) {
const date = new Date(monday);
date.setDate(monday.getDate() + i);
const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
const tile = screen.getByTestId(dateTestId);
if (date.getTime() !== today.setHours(0, 0, 0, 0)) {
expect(tile?.parentElement).toHaveClass("selectedWeek");
}
}
jest.useRealTimers();
}
);
it("renders mini calendar with the day in gray (except for today) when full calendar in day view", async () => {
renderCalendar();
// Simulate switching to day view
const dayViewButton = await screen.findByTitle(/day view/i);
fireEvent.click(dayViewButton);
const today = new Date();
const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
const todayTile = screen.getByTestId(dateTestId);
expect(todayTile?.parentElement).toHaveClass("selectedWeek");
});
it("renders mini calendar with nothing colored (except for today) when full calendar in month view", async () => {
renderCalendar();
// Switch to month view
const monthButton = await screen.findByRole("button", { name: /month/i });
fireEvent.click(monthButton);
const tiles = await screen.findAllByRole("gridcell");
tiles.forEach((tile) => {
if (!tile.classList.contains("today")) {
expect(tile.className).not.toContain("selectedWeek");
}
});
});
it("renders mini calendar with dots on days with personal events", async () => {
renderCalendar();
const dot = document.querySelector(".event-dot");
expect(dot?.parentElement?.children[0].innerHTML).toBe(
day.getDate().toString()
);
expect(dot).toBeInTheDocument();
});
});
describe("Found Bugs", () => {
const day = new Date();
beforeEach(() => {
jest.clearAllMocks();
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
});
const renderCalendar = () => {
const preloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
color: "#FF0000",
ownerEmails: ["test.test@test.com"],
events: {
event1: {
calId: "667037022b752d0026472254/cal1",
id: "event1",
title: "Test Event",
start: day.toISOString(),
},
},
},
},
pending: false,
},
};
renderWithProviders(<CalendarApp />, preloadedState);
};
it("gray day stays when day mode, click today, then change the month bar to august and come back to july", async () => {
renderCalendar();
const dayViewButton = await screen.findByTitle(/day view/i);
fireEvent.click(dayViewButton);
const nextMonthButton = screen.getByText(">");
const previousMonthButton = screen.getByText("<");
fireEvent.click(nextMonthButton);
fireEvent.click(previousMonthButton);
const selectedTile = screen.getByText((content, element) => {
return element?.classList.contains("selectedWeek") ?? false;
});
const ariaLabel = screen.getByRole("columnheader");
const shownDayDate = new Date(
ariaLabel.getAttribute("data-date") as string
);
const dateTestId = `date-${shownDayDate.getFullYear()}-${shownDayDate.getMonth()}-${shownDayDate.getDate()}`;
const supposedSelectedTile = screen.getByTestId(dateTestId);
expect(selectedTile.children[0].innerHTML).toBe(
supposedSelectedTile.parentElement?.children[0]?.innerHTML
);
expect(supposedSelectedTile?.parentElement).toHaveClass("selectedWeek");
});
it("in month view going to next month, side panel is not updated on second click to following month both components are updated with the side panel view jumping 2 months", async () => {
renderCalendar();
const monthViewButton = await screen.findByTitle(/month view/i);
fireEvent.click(monthViewButton);
const nextMonthButton = await screen.findByTitle(/Next month/i);
const previousMonthButton = await screen.findByTitle(/Previous month/i);
fireEvent.click(nextMonthButton);
const miniCalMonth = await screen.findByTitle(/mini calendar month/i);
const fullCalMonth = screen.getByText((content, element) => {
return element?.classList.contains("fc-toolbar-title") ?? false;
});
expect(miniCalMonth.innerHTML).toBe(fullCalMonth.innerHTML);
fireEvent.click(nextMonthButton);
expect(miniCalMonth.innerHTML).toBe(fullCalMonth.innerHTML);
});
it("gray day stays when day mode, change day, then change to week view", async () => {
renderCalendar();
const dayViewButton = await screen.findByTitle(/day view/i);
const weekViewButton = await screen.findByTitle(/week view/i);
fireEvent.click(dayViewButton);
const nextDayButton = screen.getByTitle("Next day");
fireEvent.click(nextDayButton);
const dayViewSelectedTiles = screen.getAllByText((content, element) => {
return element?.classList.contains("selectedWeek") ?? false;
});
expect(dayViewSelectedTiles).toHaveLength(1);
fireEvent.click(weekViewButton);
const weekViewSelectedTiles = screen.getAllByText((content, element) => {
return element?.classList.contains("selectedWeek") ?? false;
});
expect(weekViewSelectedTiles).toHaveLength(7);
});
it("gray day stays when day mode, change day, then change to week view", async () => {
jest.useFakeTimers().setSystemTime(new Date("2025-01-30"));
renderCalendar();
const nextWeekButton = screen.getByTitle("Next week");
fireEvent.click(nextWeekButton);
const miniCalMonth = await screen.findByTitle(/mini calendar month/i);
const fullCalMonth = screen.getByText((content, element) => {
return element?.classList.contains("fc-toolbar-title") ?? false;
});
expect(miniCalMonth.innerHTML).toBe(fullCalMonth.innerHTML);
});
});
View File
+5 -1
View File
@@ -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"
/>
<title>Twake Calendar</title>
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100..900;1,100..900&family=Cal+Sans&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
<title>React App</title>
<script src="<%= assetPrefix %>/.env.js"></script>
<script src="<%= assetPrefix %>/appList.js"></script>
</head>
+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>
+1 -123
View File
@@ -15,7 +15,6 @@
flex-grow: 1;
height: 100%;
overflow: hidden;
padding: 0 25px 0 10px;
}
.calendarListHeader {
@@ -26,7 +25,7 @@
}
.sidebar {
border-right: 2px solid #f3f4f6;
width: 20vw;
width: 330px;
padding: 16px 25px;
height: 90vh;
flex-direction: column;
@@ -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;
@@ -166,112 +153,3 @@
margin-left: auto;
margin-right: auto;
}
/* Custom main calendar view */
.fc-header-toolbar.fc-toolbar.fc-toolbar-ltr {
/* display: none; */
}
.fc-theme-standard td,
.fc-theme-standard th {
border: 1px solid #b8c1cc;
border-left: 0;
}
a.fc-timegrid-axis-cushion {
color: #243b55;
text-align: center;
font-family: Roboto;
font-size: 12.507px;
font-style: normal;
font-weight: 400;
line-height: 18.76px;
letter-spacing: 0.391px;
}
.fc-daygrid-day-top small {
color: #8c9caf;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
letter-spacing: 0.25px;
}
.fc-daygrid-day-top .fc-daygrid-day-number,
span.fc-daygrid-day-number {
color: #243b55;
margin: 0 8px 0 0;
font-family: Roboto;
font-size: 28px;
font-style: normal;
font-weight: 400;
line-height: 36px;
position: relative;
width: 48px;
height: 48px;
line-height: 39px;
}
.fc-daygrid-day-top .fc-daygrid-day-number:after,
span.fc-daygrid-day-number:after {
content: "";
z-index: -1;
border-radius: 50%;
width: 48px;
height: 48px;
position: absolute;
left: 0;
top: -1px;
}
span.fc-daygrid-day-number.current-date {
color: #fff;
}
span.fc-daygrid-day-number.current-date:after {
background-color: #f67e35;
}
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-scrollgrid-shrink,
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-timegrid-slot-minor {
border: 0;
}
th.fc-timegrid-axis.fc-scrollgrid-shrink {
border: 0;
border-right: 1px solid #b8c1cc;
}
.fc .fc-timegrid-axis-cushion {
min-width: 70px;
max-width: 70px;
padding: 0;
text-align: left;
}
.fc .fc-timegrid-divider {
display: none;
height: 0;
}
.fc-timegrid-slot-label-cushion {
color: #aea9b1;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
}
th.fc-col-header-cell.fc-day {
border-right: 0;
}
.fc .fc-scrollgrid {
border: 0;
}
.fc .fc-timegrid-slot-label {
justify-content: flex-end;
}
.fc .fc-timegrid-slot-label .fc-timegrid-slot-label-frame {
position: relative;
transform: translate(-15px, -12px);
}
.fc .fc-timegrid-slot-label .fc-timegrid-slot-label-frame::after {
content: "";
z-index: -1;
background-color: #b8c1cc;
width: 10px;
height: 1px;
position: absolute;
top: 9.5px;
right: -15px;
}
+338 -54
View File
@@ -6,15 +6,14 @@ import interactionPlugin from "@fullcalendar/interaction";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import ReactCalendar from "react-calendar";
import "./Calendar.css";
import { useEffect, useMemo, useRef, useState } from "react";
import "./CustomCalendar.css";
import { useEffect, useRef, 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,
@@ -39,12 +38,52 @@ 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);
// 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;
onViewChange?: (view: string) => void;
}
export default function CalendarApp({
calendarRef,
onDateChange,
onViewChange,
}: 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) {
@@ -81,6 +120,7 @@ export default function CalendarApp() {
}
});
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
const fetchedRangesRef = useRef<Record<string, string>>({});
// Auto-select personal calendars when first loaded
useEffect(() => {
@@ -114,15 +154,27 @@ export default function CalendarApp() {
);
useEffect(() => {
updateCalsDetails(
selectedCalendars,
pending,
calendars,
rangeKey,
dispatch,
calendarRange
);
}, [rangeKey, selectedCalendars]);
if (!rangeKey) return;
selectedCalendars.forEach((id) => {
if (fetchedRangesRef.current[id] === rangeKey) return;
fetchedRangesRef.current[id] = rangeKey;
dispatch(
getCalendarDetailAsync({
calId: id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
);
});
}, [
rangeKey,
selectedCalendars,
dispatch,
calendarRange.start,
calendarRange.end,
]);
useEffect(() => {
updateCalsDetails(
@@ -326,29 +378,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" } },
}}
@@ -361,10 +391,8 @@ export default function CalendarApp() {
weekNumbers
weekNumberFormat={{ week: "long" }}
slotDuration={"00:30:00"}
slotLabelInterval={"00:30:00"}
scrollTime={new Date(Date.now() - 2 * 60 * 60 * 1000)
.toTimeString()
.slice(0, 5)}
slotLabelInterval={"01:00:00"}
scrollTime="12:00:00"
unselectAuto={false}
allDayText=""
slotLabelFormat={{
@@ -373,15 +401,32 @@ 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);
}
// Notify parent about view change
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();
@@ -401,6 +446,234 @@ export default function CalendarApp() {
</div>
);
}}
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));
// 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
@@ -428,6 +701,14 @@ 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
@@ -445,8 +726,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 })
@@ -456,6 +737,14 @@ 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
@@ -473,19 +762,14 @@ export default function CalendarApp() {
);
const newEvent = {
...event,
start: computedNewStart,
end: computedNewEnd,
start: computedNewStart.toISOString(),
end: computedNewEnd.toISOString(),
} as CalendarEvent;
dispatch(
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,76 @@
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 [currentView, setCurrentView] = useState<string>("timeGridWeek");
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 handleViewChange = (view: string) => {
setCurrentView(view);
};
const menubarProps: MenubarProps = {
calendarRef,
onRefresh: handleRefresh,
currentDate,
onDateChange: handleDateChange,
currentView,
onViewChange: handleViewChange,
};
return (
<div className="App">
<Menubar {...menubarProps} />
<CalendarApp
calendarRef={calendarRef}
onDateChange={handleDateChange}
onViewChange={handleViewChange}
/>
</div>
);
}
+238
View File
@@ -0,0 +1,238 @@
/* Custom main calendar view */
.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;
font-family: Roboto;
font-size: 12.507px;
font-style: normal;
font-weight: 400;
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;
font-style: normal;
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 6px 0 0;
font-family: Roboto;
font-size: 28px;
font-style: normal;
font-weight: 400;
line-height: 36px;
position: relative;
width: 48px;
height: 48px;
line-height: 39px;
}
.fc-daygrid-day-top .fc-daygrid-day-number:after,
span.fc-daygrid-day-number:after {
content: "";
z-index: -1;
border-radius: 50%;
width: 45px;
height: 45px;
position: absolute;
top: 1px;
left: 1px;
transition: background-color 0.3s ease;
}
span.fc-daygrid-day-number.current-date {
color: #fff;
}
span.fc-daygrid-day-number.current-date:after {
background-color: #f67e35;
}
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-scrollgrid-shrink,
td.fc-timegrid-slot.fc-timegrid-slot-label.fc-timegrid-slot-minor {
border: 0;
}
th.fc-timegrid-axis.fc-scrollgrid-shrink {
border: 0;
border-right: 1px solid #b8c1cc;
}
.fc .fc-timegrid-axis-cushion {
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;
height: 0;
}
.fc-timegrid-slot-label-cushion {
color: #aea9b1;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
}
th.fc-col-header-cell.fc-day {
border-right: 0;
border-bottom: 0;
}
.fc .fc-scrollgrid {
border: 0;
}
.fc .fc-timegrid-slot-label {
justify-content: flex-end;
}
.fc .fc-timegrid-slot-label .fc-timegrid-slot-label-frame {
position: relative;
transform: translate(-15px, -12px);
}
.fc
tbody
tr:first-of-type
.fc-timegrid-slot-label
.fc-timegrid-slot-label-frame {
display: none;
}
.fc .fc-timegrid-slot-label .fc-timegrid-slot-label-frame::after {
content: "";
z-index: -1;
background-color: #b8c1cc;
width: 10px;
height: 1px;
position: absolute;
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: block !important;
width: auto;
height: auto;
position: relative;
border: 0;
}
.fc .fc-timegrid-now-indicator-arrow::before {
content: attr(data-time);
color: #f67e35;
white-space: nowrap;
font-family: Roboto, sans-serif;
font-weight: 600;
position: absolute;
top: -2px;
left: 25px;
font-size: 14px;
letter-spacing: 0.25px;
}
.fc-day-today .fc-timegrid-now-indicator-container {
overflow: unset;
}
.fc .fc-timegrid-slot {
height: auto;
min-height: 20px !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;
}
thead .fc-scroller,
tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller {
overflow: hidden !important;
}
.navigation-controls {
margin-right: 40px;
}
+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);
+12
View File
@@ -49,6 +49,7 @@
.menubar-item {
display: flex;
align-items: center;
width: calc(330px - 0.5rem);
}
.logo {
@@ -93,3 +94,14 @@
font-size: 14px;
text-align: center;
}
.left-menu {
display: flex;
align-items: center;
justify-content: flex-start;
}
.right-menu {
display: flex;
justify-content: flex-end;
align-items: center;
}
+143 -34
View File
@@ -1,11 +1,25 @@
import React, { useState } from "react";
import logo from "../../static/images/calendar.svg";
import logo from "../../static/header-logo.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,7 +27,23 @@ export type AppIconProps = {
icon: string;
};
export function Menubar() {
export type MenubarProps = {
calendarRef: React.RefObject<CalendarApi | null>;
onRefresh: () => void;
currentDate: Date;
onDateChange?: (date: Date) => void;
currentView: string;
onViewChange?: (view: string) => void;
};
export function Menubar({
calendarRef,
onRefresh,
currentDate,
onDateChange,
currentView,
onViewChange,
}: MenubarProps) {
const user = useAppSelector((state) => state.user.userData);
const applist: AppIconProps[] = (window as any).appList ?? [];
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@@ -30,37 +60,119 @@ 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;
calendarRef.current.changeView(view);
// Notify parent about view change
if (onViewChange) {
onViewChange(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 className="left-menu">
<div className="menu-items">
<MainTitle />
</div>
<div className="menu-items">
<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>
<div className="menu-items">
<div className="current-date-time">
<Typography variant="h6" component="div">
{currentDate.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})}
</Typography>
</div>
</div>
</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="right-menu">
<div className="menu-items">
<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>
</div>
<div className="menu-items">
{applist.length > 0 && (
<IconButton onClick={handleOpen} sx={{ mr: 1 }}>
<AppsIcon />
</IconButton>
)}
</div>
<div className="menu-items">
<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,11 +200,7 @@ export function Menubar() {
export function MainTitle() {
return (
<div className="menubar-item tc-home">
<img className="logo" src={logo} />
<p>
<span className="twake">Twake</span>
<span className="calendar-text">Calendar</span>
</p>
<img className="logo" src={logo} alt="Calendar" />
</div>
);
}
@@ -102,6 +210,7 @@ function AppIcon({ prop }: { prop: AppIconProps }) {
<a
href={prop.link}
target="_blank"
rel="noreferrer"
style={{ textDecoration: "none", color: "inherit" }}
>
<div>
+2 -8
View File
@@ -70,14 +70,8 @@ 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()
);
const [end, setEnd] = useState(
event?.end ? new Date(event.end)?.toISOString() : new Date().toISOString()
);
const [start, setStart] = useState(event?.start ? event.start : "");
const [end, setEnd] = useState(event?.end ? event.end : "");
const [calendarid, setCalendarid] = useState(
event?.calId
? userPersonnalCalendars.findIndex((e) => e.id === 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;
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB