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 * as eventThunks from "../../src/features/Calendars/CalendarSlice";
|
||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
import { searchUsers } from "../../src/features/User/userAPI";
|
import { searchUsers } from "../../src/features/User/userAPI";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
jest.mock("../../src/features/User/userAPI");
|
jest.mock("../../src/features/User/userAPI");
|
||||||
@@ -10,6 +11,12 @@ const mockedSearchUsers = searchUsers as jest.MockedFunction<
|
|||||||
typeof searchUsers
|
typeof searchUsers
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Test wrapper component to provide calendarRef
|
||||||
|
function CalendarTestWrapper() {
|
||||||
|
const calendarRef = useRef(null);
|
||||||
|
return <CalendarApp calendarRef={calendarRef} />;
|
||||||
|
}
|
||||||
|
|
||||||
describe("CalendarSelection", () => {
|
describe("CalendarSelection", () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const start = new Date(today);
|
const start = new Date(today);
|
||||||
@@ -127,7 +134,11 @@ describe("CalendarSelection", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
it("renders calendars", async () => {
|
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("Personnal Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||||
@@ -136,26 +147,12 @@ describe("CalendarSelection", () => {
|
|||||||
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("Calendar shared")).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", () => {
|
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("Personnal Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Other 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);
|
const input = screen.getByPlaceholderText(/search user/i);
|
||||||
userEvent.type(input, "New");
|
userEvent.type(input, "New");
|
||||||
@@ -243,7 +240,7 @@ describe("calendar Availability search", () => {
|
|||||||
.mockImplementation((payload) => {
|
.mockImplementation((payload) => {
|
||||||
return () => Promise.resolve(payload) as any;
|
return () => Promise.resolve(payload) as any;
|
||||||
});
|
});
|
||||||
renderWithProviders(<CalendarApp />, preloadedState);
|
renderWithProviders(<CalendarTestWrapper />, preloadedState);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText(/search user/i);
|
const input = screen.getByPlaceholderText(/search user/i);
|
||||||
userEvent.type(input, "Alice");
|
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 () => {
|
it("renders the event on the calendar and calendarRef works", async () => {
|
||||||
@@ -157,7 +161,11 @@ describe("CalendarApp integration", () => {
|
|||||||
class: "PRIVATE",
|
class: "PRIVATE",
|
||||||
title: "Private Event",
|
title: "Private Event",
|
||||||
});
|
});
|
||||||
renderWithProviders(<CalendarApp />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
|
||||||
const eventEls = screen.getAllByText("Private Event");
|
const eventEls = screen.getAllByText("Private Event");
|
||||||
const found = eventEls.some((eventEl) =>
|
const found = eventEls.some((eventEl) =>
|
||||||
@@ -171,7 +179,11 @@ describe("CalendarApp integration", () => {
|
|||||||
class: "CONFIDENTIAL",
|
class: "CONFIDENTIAL",
|
||||||
title: "Confidential Event",
|
title: "Confidential Event",
|
||||||
});
|
});
|
||||||
renderWithProviders(<CalendarApp />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
|
||||||
const eventEls = screen.getAllByText("Confidential Event");
|
const eventEls = screen.getAllByText("Confidential Event");
|
||||||
const found = eventEls.some((eventEl) =>
|
const found = eventEls.some((eventEl) =>
|
||||||
@@ -185,7 +197,11 @@ describe("CalendarApp integration", () => {
|
|||||||
class: "PUBLIC",
|
class: "PUBLIC",
|
||||||
title: "Public Event",
|
title: "Public Event",
|
||||||
});
|
});
|
||||||
renderWithProviders(<CalendarApp />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
|
||||||
const eventEls = screen.getAllByText("Public Event");
|
const eventEls = screen.getAllByText("Public Event");
|
||||||
const found = eventEls.some((eventEl) =>
|
const found = eventEls.some((eventEl) =>
|
||||||
|
|||||||
@@ -21,20 +21,55 @@ describe("Calendar App Component Display Tests", () => {
|
|||||||
{ name: "Twake", link: "/twake", icon: "twake.svg" },
|
{ name: "Twake", link: "/twake", icon: "twake.svg" },
|
||||||
{ name: "Calendar", link: "/calendar", icon: "calendar.svg" },
|
{ name: "Calendar", link: "/calendar", icon: "calendar.svg" },
|
||||||
];
|
];
|
||||||
renderWithProviders(<Menubar />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
const navbarElement = screen.getByText("Twake");
|
const mockOnRefresh = jest.fn();
|
||||||
expect(navbarElement).toBeInTheDocument();
|
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", () => {
|
it("renders the main title", () => {
|
||||||
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
renderWithProviders(<Menubar />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
expect(screen.getByText(/Twake/i)).toBeInTheDocument();
|
const mockOnRefresh = jest.fn();
|
||||||
expect(screen.getByText(/Calendar/i)).toBeInTheDocument();
|
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", () => {
|
it("shows avatar with user initials", () => {
|
||||||
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
(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();
|
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();
|
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();
|
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();
|
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();
|
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();
|
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
|
// Should not crash and show empty string
|
||||||
const avatar = screen.getByRole("img", { hidden: true });
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
expect(avatar).toBeInTheDocument();
|
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
|
// Should not crash and show empty string
|
||||||
const avatar = screen.getByRole("img", { hidden: true });
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
expect(avatar).toBeInTheDocument();
|
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
|
// Should not crash and show empty string
|
||||||
const avatar = screen.getByRole("img", { hidden: true });
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
expect(avatar).toBeInTheDocument();
|
expect(avatar).toBeInTheDocument();
|
||||||
@@ -190,14 +321,41 @@ describe("Calendar App Component Display Tests", () => {
|
|||||||
|
|
||||||
it("shows AppsIcon when applist is not empty", () => {
|
it("shows AppsIcon when applist is not empty", () => {
|
||||||
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
(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();
|
expect(screen.getByTestId("AppsIcon")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens popover when clicking AppsIcon", () => {
|
it("opens popover when clicking AppsIcon", () => {
|
||||||
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
(window as any).appList = [
|
||||||
renderWithProviders(<Menubar />, preloadedState);
|
{ name: "Twake", icon: "twake.svg", link: "/twake" },
|
||||||
const appsButton = screen.getByRole("button");
|
{ 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);
|
fireEvent.click(appsButton);
|
||||||
expect(screen.getByText("Twake")).toBeInTheDocument();
|
expect(screen.getByText("Twake")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
expect(screen.getByText("Calendar")).toBeInTheDocument();
|
||||||
@@ -205,11 +363,23 @@ describe("Calendar App Component Display Tests", () => {
|
|||||||
|
|
||||||
it("renders app icons as links", () => {
|
it("renders app icons as links", () => {
|
||||||
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
renderWithProviders(<Menubar />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
const appsButton = screen.getByRole("button");
|
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);
|
fireEvent.click(appsButton);
|
||||||
|
|
||||||
const twakeLink = screen.getByRole("link", { name: /test/i });
|
const testLink = screen.getByRole("link", { name: /test/i });
|
||||||
expect(twakeLink).toHaveAttribute("href", "test");
|
expect(testLink).toHaveAttribute("href", "test");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
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 { jest } from "@jest/globals";
|
||||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||||
import * as appHooks from "../../src/app/hooks";
|
import * as appHooks from "../../src/app/hooks";
|
||||||
@@ -44,7 +44,11 @@ describe("MiniCalendar", () => {
|
|||||||
pending: false,
|
pending: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
renderWithProviders(<CalendarApp />, preloadedState);
|
const mockCalendarRef = { current: null };
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders mini calendar with today in orange", async () => {
|
it("renders mini calendar with today in orange", async () => {
|
||||||
@@ -55,193 +59,4 @@ describe("MiniCalendar", () => {
|
|||||||
const todayTile = screen.getByTestId(dateTestId);
|
const todayTile = screen.getByTestId(dateTestId);
|
||||||
expect(todayTile?.parentElement).toHaveClass("today");
|
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"
|
href="https://fonts.googleapis.com/css2?family=Cal+Sans&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
|
||||||
rel="stylesheet"
|
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 %>/.env.js"></script>
|
||||||
<script src="<%= assetPrefix %>/appList.js"></script>
|
<script src="<%= assetPrefix %>/appList.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+2
-11
@@ -1,13 +1,12 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
||||||
import { Menubar } from "./components/Menubar/Menubar";
|
|
||||||
import { CallbackResume } from "./features/User/LoginCallback";
|
import { CallbackResume } from "./features/User/LoginCallback";
|
||||||
import { history } from "./app/store";
|
import { history } from "./app/store";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Loading } from "./components/Loading/Loading";
|
import { Loading } from "./components/Loading/Loading";
|
||||||
import HandleLogin from "./features/User/HandleLogin";
|
import HandleLogin from "./features/User/HandleLogin";
|
||||||
import CalendarApp from "./components/Calendar/Calendar";
|
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||||
import { Error } from "./components/Error/Error";
|
import { Error } from "./components/Error/Error";
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -15,15 +14,7 @@ function App() {
|
|||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HandleLogin />} />
|
<Route path="/" element={<HandleLogin />} />
|
||||||
<Route
|
<Route path="/calendar" element={<CalendarLayout />} />
|
||||||
path="/calendar"
|
|
||||||
element={
|
|
||||||
<div className="App">
|
|
||||||
<Menubar />
|
|
||||||
<CalendarApp />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/callback" element={<CallbackResume />} />
|
<Route path="/callback" element={<CallbackResume />} />
|
||||||
<Route path="/error" element={<Error />} />
|
<Route path="/error" element={<Error />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 25px 0 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendarListHeader {
|
.calendarListHeader {
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 2px solid #f3f4f6;
|
border-right: 2px solid #f3f4f6;
|
||||||
width: 20vw;
|
width: 330px;
|
||||||
padding: 16px 25px;
|
padding: 16px 25px;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -42,18 +41,6 @@
|
|||||||
border: dashed 1px #ffffff;
|
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-event-main span,
|
||||||
.fc-daygrid-event div {
|
.fc-daygrid-event div {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
@@ -166,112 +153,3 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 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 { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||||
import ReactCalendar from "react-calendar";
|
import ReactCalendar from "react-calendar";
|
||||||
import "./Calendar.css";
|
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 { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import EventPopover from "../../features/Events/EventModal";
|
import EventPopover from "../../features/Events/EventModal";
|
||||||
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
import CalendarSelection from "./CalendarSelection";
|
import CalendarSelection from "./CalendarSelection";
|
||||||
import {
|
import {
|
||||||
getCalendarDetailAsync,
|
getCalendarDetailAsync,
|
||||||
getCalendarsListAsync,
|
|
||||||
getEventAsync,
|
getEventAsync,
|
||||||
putEventAsync,
|
putEventAsync,
|
||||||
updateEventLocal,
|
updateEventLocal,
|
||||||
@@ -39,12 +38,52 @@ import { userAttendee } from "../../features/User/userDataTypes";
|
|||||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
export default function CalendarApp() {
|
// Function to hide/show slot labels based on current time
|
||||||
const calendarRef = useRef<CalendarApi | null>(null);
|
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 [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||||
const tokens = useAppSelector((state) => state.user.tokens);
|
const tokens = useAppSelector((state) => state.user.tokens);
|
||||||
const user = useAppSelector((state) => state.user.userData);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
if (!tokens) {
|
if (!tokens) {
|
||||||
@@ -81,6 +120,7 @@ export default function CalendarApp() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
||||||
|
const fetchedRangesRef = useRef<Record<string, string>>({});
|
||||||
|
|
||||||
// Auto-select personal calendars when first loaded
|
// Auto-select personal calendars when first loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -114,15 +154,27 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCalsDetails(
|
if (!rangeKey) return;
|
||||||
selectedCalendars,
|
selectedCalendars.forEach((id) => {
|
||||||
pending,
|
if (fetchedRangesRef.current[id] === rangeKey) return;
|
||||||
calendars,
|
fetchedRangesRef.current[id] = rangeKey;
|
||||||
rangeKey,
|
dispatch(
|
||||||
dispatch,
|
getCalendarDetailAsync({
|
||||||
calendarRange
|
calId: id,
|
||||||
);
|
match: {
|
||||||
}, [rangeKey, selectedCalendars]);
|
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||||
|
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
rangeKey,
|
||||||
|
selectedCalendars,
|
||||||
|
dispatch,
|
||||||
|
calendarRange.start,
|
||||||
|
calendarRange.end,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCalsDetails(
|
updateCalsDetails(
|
||||||
@@ -326,29 +378,7 @@ export default function CalendarApp() {
|
|||||||
height={"100%"}
|
height={"100%"}
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
nowIndicator
|
nowIndicator
|
||||||
customButtons={{
|
headerToolbar={false}
|
||||||
refresh: {
|
|
||||||
text: "↻",
|
|
||||||
click: async () => {
|
|
||||||
await dispatch(getCalendarsListAsync());
|
|
||||||
selectedCalendars.forEach((id) => {
|
|
||||||
if (!pending && rangeKey) {
|
|
||||||
dispatch(
|
|
||||||
getCalendarDetailAsync({
|
|
||||||
calId: id,
|
|
||||||
match: {
|
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(
|
|
||||||
calendarRange.start
|
|
||||||
),
|
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
views={{
|
views={{
|
||||||
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
||||||
}}
|
}}
|
||||||
@@ -361,10 +391,8 @@ export default function CalendarApp() {
|
|||||||
weekNumbers
|
weekNumbers
|
||||||
weekNumberFormat={{ week: "long" }}
|
weekNumberFormat={{ week: "long" }}
|
||||||
slotDuration={"00:30:00"}
|
slotDuration={"00:30:00"}
|
||||||
slotLabelInterval={"00:30:00"}
|
slotLabelInterval={"01:00:00"}
|
||||||
scrollTime={new Date(Date.now() - 2 * 60 * 60 * 1000)
|
scrollTime="12:00:00"
|
||||||
.toTimeString()
|
|
||||||
.slice(0, 5)}
|
|
||||||
unselectAuto={false}
|
unselectAuto={false}
|
||||||
allDayText=""
|
allDayText=""
|
||||||
slotLabelFormat={{
|
slotLabelFormat={{
|
||||||
@@ -373,15 +401,32 @@ export default function CalendarApp() {
|
|||||||
hour12: false,
|
hour12: false,
|
||||||
}}
|
}}
|
||||||
datesSet={(arg) => {
|
datesSet={(arg) => {
|
||||||
|
// Get the current date from calendar API to ensure consistency
|
||||||
|
const calendarCurrentDate =
|
||||||
|
calendarRef.current?.getDate() || new Date(arg.start);
|
||||||
|
|
||||||
if (arg.view.type === "dayGridMonth") {
|
if (arg.view.type === "dayGridMonth") {
|
||||||
setSelectedDate(new Date(arg.start));
|
setSelectedDate(new Date(arg.start));
|
||||||
const midTimestamp =
|
setSelectedMiniDate(calendarCurrentDate);
|
||||||
(arg.start.getTime() + arg.end.getTime()) / 2;
|
|
||||||
setSelectedMiniDate(new Date(midTimestamp));
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedDate(new Date(arg.start));
|
setSelectedDate(new Date(arg.start));
|
||||||
setSelectedMiniDate(new Date(arg.start));
|
setSelectedMiniDate(new Date(arg.start));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always use the calendar's current date for consistency
|
||||||
|
if (onDateChange) {
|
||||||
|
onDateChange(calendarCurrentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
dayHeaderContent={(arg) => {
|
||||||
const date = arg.date.getDate();
|
const date = arg.date.getDate();
|
||||||
@@ -401,6 +446,234 @@ export default function CalendarApp() {
|
|||||||
</div>
|
</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) => {
|
eventClick={(info) => {
|
||||||
info.jsEvent.preventDefault(); // don't let the browser navigate
|
info.jsEvent.preventDefault(); // don't let the browser navigate
|
||||||
|
|
||||||
@@ -428,6 +701,14 @@ export default function CalendarApp() {
|
|||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
eventDrop={(arg) => {
|
eventDrop={(arg) => {
|
||||||
|
if (
|
||||||
|
!arg.event ||
|
||||||
|
!arg.event._def ||
|
||||||
|
!arg.event._def.extendedProps
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const event =
|
const event =
|
||||||
calendars[arg.event._def.extendedProps.calId].events[
|
calendars[arg.event._def.extendedProps.calId].events[
|
||||||
arg.event._def.extendedProps.uid
|
arg.event._def.extendedProps.uid
|
||||||
@@ -445,8 +726,8 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
const newEvent = {
|
const newEvent = {
|
||||||
...event,
|
...event,
|
||||||
start: computedNewStart,
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd,
|
end: computedNewEnd.toISOString(),
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
dispatch(
|
dispatch(
|
||||||
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
updateEventLocal({ calId: newEvent.calId, event: newEvent })
|
||||||
@@ -456,6 +737,14 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
eventResize={(arg) => {
|
eventResize={(arg) => {
|
||||||
|
if (
|
||||||
|
!arg.event ||
|
||||||
|
!arg.event._def ||
|
||||||
|
!arg.event._def.extendedProps
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const event =
|
const event =
|
||||||
calendars[arg.event._def.extendedProps.calId].events[
|
calendars[arg.event._def.extendedProps.calId].events[
|
||||||
arg.event._def.extendedProps.uid
|
arg.event._def.extendedProps.uid
|
||||||
@@ -473,19 +762,14 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
const newEvent = {
|
const newEvent = {
|
||||||
...event,
|
...event,
|
||||||
start: computedNewStart,
|
start: computedNewStart.toISOString(),
|
||||||
end: computedNewEnd,
|
end: computedNewEnd.toISOString(),
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
headerToolbar={{
|
|
||||||
left: "title",
|
|
||||||
center: "prev,today,next",
|
|
||||||
right: "refresh,dayGridMonth,timeGridWeek,timeGridDay",
|
|
||||||
}}
|
|
||||||
eventContent={(arg) => {
|
eventContent={(arg) => {
|
||||||
const event = arg.event;
|
const event = arg.event;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -0,0 +1,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 [openModal, setOpenModal] = useState(false);
|
||||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>({
|
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>({
|
||||||
start: new Date(event.start),
|
start: new Date(event.start),
|
||||||
startStr: new Date(event.start).toISOString(),
|
startStr: event.start,
|
||||||
end: new Date(event.end ?? ""),
|
end: new Date(event.end ?? ""),
|
||||||
endStr: new Date(event.end ?? "").toISOString(),
|
endStr: event.end ?? "",
|
||||||
allDay: event.allday ?? false,
|
allDay: event.allday ?? false,
|
||||||
} as DateSelectArg);
|
} as DateSelectArg);
|
||||||
const calendarRef = useRef<CalendarApi | null>(null);
|
const calendarRef = useRef<CalendarApi | null>(null);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
.menubar-item {
|
.menubar-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: calc(330px - 0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -93,3 +94,14 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
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 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 AppsIcon from "@mui/icons-material/Apps";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import "./Menubar.css";
|
import "./Menubar.css";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { stringToColor } from "../../features/Events/EventDisplay";
|
import { stringToColor } from "../../features/Events/EventDisplay";
|
||||||
import { Avatar, IconButton, Popover } from "@mui/material";
|
import {
|
||||||
|
Avatar,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
ButtonGroup,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
|
import { CalendarApi } from "@fullcalendar/core";
|
||||||
|
|
||||||
export type AppIconProps = {
|
export type AppIconProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,7 +27,23 @@ export type AppIconProps = {
|
|||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Menubar() {
|
export type MenubarProps = {
|
||||||
|
calendarRef: React.RefObject<CalendarApi | null>;
|
||||||
|
onRefresh: () => void;
|
||||||
|
currentDate: Date;
|
||||||
|
onDateChange?: (date: Date) => void;
|
||||||
|
currentView: string;
|
||||||
|
onViewChange?: (view: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Menubar({
|
||||||
|
calendarRef,
|
||||||
|
onRefresh,
|
||||||
|
currentDate,
|
||||||
|
onDateChange,
|
||||||
|
currentView,
|
||||||
|
onViewChange,
|
||||||
|
}: MenubarProps) {
|
||||||
const user = useAppSelector((state) => state.user.userData);
|
const user = useAppSelector((state) => state.user.userData);
|
||||||
const applist: AppIconProps[] = (window as any).appList ?? [];
|
const applist: AppIconProps[] = (window as any).appList ?? [];
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@@ -30,37 +60,119 @@ export function Menubar() {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNavigation = (action: "prev" | "next" | "today") => {
|
||||||
|
if (!calendarRef.current) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "prev":
|
||||||
|
calendarRef.current.prev();
|
||||||
|
break;
|
||||||
|
case "next":
|
||||||
|
calendarRef.current.next();
|
||||||
|
break;
|
||||||
|
case "today":
|
||||||
|
calendarRef.current.today();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify parent about date change after navigation
|
||||||
|
if (onDateChange) {
|
||||||
|
const newDate = calendarRef.current.getDate();
|
||||||
|
onDateChange(newDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewChange = (view: string) => {
|
||||||
|
if (!calendarRef.current) return;
|
||||||
|
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);
|
const open = Boolean(anchorEl);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="menubar">
|
<header className="menubar">
|
||||||
<MainTitle />
|
<div className="left-menu">
|
||||||
<div className="menubar-item search-bar">
|
<div className="menu-items">
|
||||||
<p>big search bar</p>
|
<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>
|
||||||
|
<div className="right-menu">
|
||||||
<div className="menubar-item">
|
<div className="menu-items">
|
||||||
{applist.length > 0 && (
|
<FormControl size="small" sx={{ minWidth: 120, mr: 2 }}>
|
||||||
<IconButton onClick={handleOpen}>
|
<Select
|
||||||
<AppsIcon />
|
value={currentView}
|
||||||
</IconButton>
|
onChange={(e) => handleViewChange(e.target.value)}
|
||||||
)}
|
variant="outlined"
|
||||||
<Avatar
|
>
|
||||||
sx={{
|
<MenuItem value="dayGridMonth">Month</MenuItem>
|
||||||
bgcolor: stringToColor(
|
<MenuItem value="timeGridWeek">Week</MenuItem>
|
||||||
user && user.family_name
|
<MenuItem value="timeGridDay">Day</MenuItem>
|
||||||
? user.family_name
|
</Select>
|
||||||
: user && user.email
|
</FormControl>
|
||||||
? user.email
|
</div>
|
||||||
: ""
|
<div className="menu-items">
|
||||||
),
|
{applist.length > 0 && (
|
||||||
}}
|
<IconButton onClick={handleOpen} sx={{ mr: 1 }}>
|
||||||
sizes="large"
|
<AppsIcon />
|
||||||
>
|
</IconButton>
|
||||||
{user?.name && user?.family_name
|
)}
|
||||||
? `${user.name[0]}${user.family_name[0]}`
|
</div>
|
||||||
: (user?.email?.[0] ?? "")}
|
<div className="menu-items">
|
||||||
</Avatar>
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
bgcolor: stringToColor(
|
||||||
|
user && user.family_name
|
||||||
|
? user.family_name
|
||||||
|
: user && user.email
|
||||||
|
? user.email
|
||||||
|
: ""
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sizes="large"
|
||||||
|
>
|
||||||
|
{user?.name && user?.family_name
|
||||||
|
? `${user.name[0]}${user.family_name[0]}`
|
||||||
|
: (user?.email?.[0] ?? "")}
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -88,11 +200,7 @@ export function Menubar() {
|
|||||||
export function MainTitle() {
|
export function MainTitle() {
|
||||||
return (
|
return (
|
||||||
<div className="menubar-item tc-home">
|
<div className="menubar-item tc-home">
|
||||||
<img className="logo" src={logo} />
|
<img className="logo" src={logo} alt="Calendar" />
|
||||||
<p>
|
|
||||||
<span className="twake">Twake</span>
|
|
||||||
<span className="calendar-text">Calendar</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,6 +210,7 @@ function AppIcon({ prop }: { prop: AppIconProps }) {
|
|||||||
<a
|
<a
|
||||||
href={prop.link}
|
href={prop.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
style={{ textDecoration: "none", color: "inherit" }}
|
style={{ textDecoration: "none", color: "inherit" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -70,14 +70,8 @@ function EventPopover({
|
|||||||
|
|
||||||
const [description, setDescription] = useState(event?.description ?? "");
|
const [description, setDescription] = useState(event?.description ?? "");
|
||||||
const [location, setLocation] = useState(event?.location ?? "");
|
const [location, setLocation] = useState(event?.location ?? "");
|
||||||
const [start, setStart] = useState(
|
const [start, setStart] = useState(event?.start ? event.start : "");
|
||||||
event?.start
|
const [end, setEnd] = useState(event?.end ? event.end : "");
|
||||||
? new Date(event.start).toISOString()
|
|
||||||
: new Date().toISOString()
|
|
||||||
);
|
|
||||||
const [end, setEnd] = useState(
|
|
||||||
event?.end ? new Date(event.end)?.toISOString() : new Date().toISOString()
|
|
||||||
);
|
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
event?.calId
|
event?.calId
|
||||||
? userPersonnalCalendars.findIndex((e) => e.id === event?.calId)
|
? userPersonnalCalendars.findIndex((e) => e.id === event?.calId)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export interface CalendarEvent {
|
|||||||
calId: string;
|
calId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
transp?: string;
|
transp?: string;
|
||||||
start: Date; // ISO date
|
start: string; // ISO date string
|
||||||
end?: Date;
|
end?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
x_openpass_videoconference?: string;
|
x_openpass_videoconference?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -14,7 +14,7 @@ export interface CalendarEvent {
|
|||||||
location?: string;
|
location?: string;
|
||||||
organizer?: userOrganiser;
|
organizer?: userOrganiser;
|
||||||
attendee: userAttendee[];
|
attendee: userAttendee[];
|
||||||
stamp?: Date;
|
stamp?: string;
|
||||||
sequence?: Number;
|
sequence?: Number;
|
||||||
color?: string;
|
color?: string;
|
||||||
allday?: boolean;
|
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