diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx index 965c40c..8417c68 100644 --- a/__test__/components/Calendar.test.tsx +++ b/__test__/components/Calendar.test.tsx @@ -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 ; +} + describe("CalendarSelection", () => { const today = new Date(); const start = new Date(today); @@ -127,7 +134,11 @@ describe("CalendarSelection", () => { }, }; it("renders calendars", async () => { - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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(, preloadedState); - - const refreshButton = screen.getByRole("button", { - name: "↻", - }); - - fireEvent.click(refreshButton); - - await waitFor(() => { - expect(spy).toHaveBeenCalled(); - }); - }); it("open accordeon when clicking on button only", () => { - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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(, preloadedState); + renderWithProviders(, 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(, preloadedState); + renderWithProviders(, preloadedState); const input = screen.getByPlaceholderText(/search user/i); userEvent.type(input, "Alice"); diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx index 731d303..d76b2ab 100644 --- a/__test__/components/EventModifications.test.tsx +++ b/__test__/components/EventModifications.test.tsx @@ -69,7 +69,11 @@ describe("CalendarApp integration", () => { }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + preloadedState + ); const eventEls = screen.getAllByText("Public Event"); const found = eventEls.some((eventEl) => diff --git a/__test__/components/Menubar.test.tsx b/__test__/components/Menubar.test.tsx index 55a5d6c..cb6e8ed 100644 --- a/__test__/components/Menubar.test.tsx +++ b/__test__/components/Menubar.test.tsx @@ -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(, 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( + , + 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(, 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( + , + preloadedState + ); + expect(screen.getByAltText("Calendar")).toBeInTheDocument(); }); it("shows avatar with user initials", () => { (window as any).appList = [{ name: "test", icon: "test", link: "test" }]; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("JD")).toBeInTheDocument(); }); @@ -51,7 +86,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("t")).toBeInTheDocument(); }); @@ -70,7 +117,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("t")).toBeInTheDocument(); }); @@ -88,7 +147,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("t")).toBeInTheDocument(); }); @@ -106,7 +177,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("JD")).toBeInTheDocument(); }); @@ -124,7 +207,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByText("t")).toBeInTheDocument(); }); @@ -142,7 +237,19 @@ describe("Calendar App Component Display Tests", () => { }, }, }; - renderWithProviders(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + 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(, preloadedState); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + preloadedState + ); expect(screen.getByTestId("AppsIcon")).toBeInTheDocument(); }); it("opens popover when clicking AppsIcon", () => { - (window as any).appList = [{ name: "test", icon: "test", link: "test" }]; - renderWithProviders(, 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( + , + 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(, preloadedState); - const appsButton = screen.getByRole("button"); + const mockCalendarRef = { current: null }; + const mockOnRefresh = jest.fn(); + const mockCurrentDate = new Date("2024-04-15"); + + renderWithProviders( + , + 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"); }); }); diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 1c3fd97..4d51021 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -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(, preloadedState); + const mockCalendarRef = { current: null }; + renderWithProviders( + , + 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; - 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(, 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); - }); }); diff --git a/babel.config.ts b/babel.config.js similarity index 100% rename from babel.config.ts rename to babel.config.js diff --git a/public/index.html b/public/index.html index 90cace4..65fbf91 100644 --- a/public/index.html +++ b/public/index.html @@ -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" /> - Twake Calendar + + React App diff --git a/src/App.tsx b/src/App.tsx index 449cb1c..441f905 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> - - - - - } - /> + } /> } /> } /> diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index 41233fd..df509e2 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -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; -} diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index e06f9d3..08c31d3 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -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(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; + 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([]); + const fetchedRangesRef = useRef>({}); // 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() { ); }} + 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 ( diff --git a/src/components/Calendar/CalendarLayout.tsx b/src/components/Calendar/CalendarLayout.tsx new file mode 100644 index 0000000..917fc87 --- /dev/null +++ b/src/components/Calendar/CalendarLayout.tsx @@ -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(null); + const dispatch = useAppDispatch(); + const selectedCalendars = useAppSelector((state) => state.calendars.list); + const userId = + useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; + const [currentDate, setCurrentDate] = useState(new Date()); + const [currentView, setCurrentView] = useState("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 ( +
+ + +
+ ); +} diff --git a/src/components/Calendar/CustomCalendar.css b/src/components/Calendar/CustomCalendar.css new file mode 100644 index 0000000..c33b828 --- /dev/null +++ b/src/components/Calendar/CustomCalendar.css @@ -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; +} diff --git a/src/components/Event/EventDuplicate.tsx b/src/components/Event/EventDuplicate.tsx index 9e5d453..af02c3a 100644 --- a/src/components/Event/EventDuplicate.tsx +++ b/src/components/Event/EventDuplicate.tsx @@ -15,9 +15,9 @@ export default function EventDuplication({ const [openModal, setOpenModal] = useState(false); const [selectedRange, setSelectedRange] = useState({ 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(null); diff --git a/src/components/Menubar/Menubar.css b/src/components/Menubar/Menubar.css index 2fcffb0..617bc20 100644 --- a/src/components/Menubar/Menubar.css +++ b/src/components/Menubar/Menubar.css @@ -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; +} diff --git a/src/components/Menubar/Menubar.tsx b/src/components/Menubar/Menubar.tsx index 3850f71..8ee58e0 100644 --- a/src/components/Menubar/Menubar.tsx +++ b/src/components/Menubar/Menubar.tsx @@ -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; + 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); @@ -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 ( <>
- -
-

big search bar

+
+
+ +
+
+
+ + + + + +
+
+
+
+ + {currentDate.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + })} + +
+
- -
- {applist.length > 0 && ( - - - - )} - - {user?.name && user?.family_name - ? `${user.name[0]}${user.family_name[0]}` - : (user?.email?.[0] ?? "")} - +
+
+ + + +
+
+ {applist.length > 0 && ( + + + + )} +
+
+ + {user?.name && user?.family_name + ? `${user.name[0]}${user.family_name[0]}` + : (user?.email?.[0] ?? "")} + +
- -

- Twake - Calendar -

+ Calendar ); } @@ -102,6 +210,7 @@ function AppIcon({ prop }: { prop: AppIconProps }) {
diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 1822927..abedf74 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -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) diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 94d1ee8..13105a3 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -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; diff --git a/src/static/header-logo.svg b/src/static/header-logo.svg new file mode 100644 index 0000000..a0a7f74 --- /dev/null +++ b/src/static/header-logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +