Merge pull request #130 from linagora/UI/style-main-grid
UI/style main grid
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user