@@ -55,6 +55,139 @@ describe("Calendar App Component Display Tests", () => {
|
|||||||
expect(screen.getByText("t")).toBeInTheDocument();
|
expect(screen.getByText("t")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edge cases for avatar display logic
|
||||||
|
it("handles user with only family_name", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Smith",
|
||||||
|
name: null,
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
expect(screen.getByText("t")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with only name", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: null,
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
expect(screen.getByText("t")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with both name and family_name", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Doe",
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
expect(screen.getByText("JD")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with no name and family_name", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: null,
|
||||||
|
name: null,
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
expect(screen.getByText("t")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with empty email", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "",
|
||||||
|
family_name: null,
|
||||||
|
name: null,
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
// Should not crash and show empty string
|
||||||
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
|
expect(avatar).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with null email", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: null,
|
||||||
|
family_name: null,
|
||||||
|
name: null,
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
// Should not crash and show empty string
|
||||||
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
|
expect(avatar).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles user with undefined email", () => {
|
||||||
|
(window as any).appList = [{ name: "test", icon: "test", link: "test" }];
|
||||||
|
const preloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: undefined,
|
||||||
|
family_name: null,
|
||||||
|
name: null,
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
// Should not crash and show empty string
|
||||||
|
const avatar = screen.getByRole("img", { hidden: true });
|
||||||
|
expect(avatar).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
renderWithProviders(<Menubar />, preloadedState);
|
||||||
|
|||||||
@@ -465,4 +465,202 @@ describe("calendarEventToJCal", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edge cases for timezone fallback
|
||||||
|
it("handles invalid timezone with UTC fallback", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
uid: "event-invalid-tz",
|
||||||
|
title: "Invalid Timezone Event",
|
||||||
|
start: new Date("2025-07-23T10:00:00"),
|
||||||
|
end: new Date("2025-07-23T11:00:00"),
|
||||||
|
timezone: "Invalid/Timezone",
|
||||||
|
allday: false,
|
||||||
|
attendee: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = calendarEventToJCal(mockEvent as CalendarEvent);
|
||||||
|
|
||||||
|
expect(result[0]).toBe("vcalendar");
|
||||||
|
const [vevent, vtimezone] = result[2];
|
||||||
|
expect(vevent[0]).toBe("vevent");
|
||||||
|
expect(vtimezone[0]).toBe("vtimezone");
|
||||||
|
|
||||||
|
// Should use UTC timezone as fallback - check for UTC standard timezone
|
||||||
|
expect(vtimezone[2]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
[
|
||||||
|
"standard",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzname", {}, "text", "UTC"],
|
||||||
|
["dtstart", {}, "date-time", "1970-01-01T00:00:00"],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null timezone with UTC fallback", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
uid: "event-null-tz",
|
||||||
|
title: "Null Timezone Event",
|
||||||
|
start: new Date("2025-07-23T10:00:00"),
|
||||||
|
end: new Date("2025-07-23T11:00:00"),
|
||||||
|
timezone: null,
|
||||||
|
allday: false,
|
||||||
|
attendee: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = calendarEventToJCal(mockEvent as CalendarEvent);
|
||||||
|
|
||||||
|
expect(result[0]).toBe("vcalendar");
|
||||||
|
const [vevent, vtimezone] = result[2];
|
||||||
|
expect(vevent[0]).toBe("vevent");
|
||||||
|
expect(vtimezone[0]).toBe("vtimezone");
|
||||||
|
|
||||||
|
// Should use UTC timezone as fallback
|
||||||
|
expect(vtimezone[2]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
[
|
||||||
|
"standard",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzname", {}, "text", "UTC"],
|
||||||
|
["dtstart", {}, "date-time", "1970-01-01T00:00:00"],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined timezone with UTC fallback", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
uid: "event-undefined-tz",
|
||||||
|
title: "Undefined Timezone Event",
|
||||||
|
start: new Date("2025-07-23T10:00:00"),
|
||||||
|
end: new Date("2025-07-23T11:00:00"),
|
||||||
|
timezone: undefined,
|
||||||
|
allday: false,
|
||||||
|
attendee: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = calendarEventToJCal(mockEvent as CalendarEvent);
|
||||||
|
|
||||||
|
expect(result[0]).toBe("vcalendar");
|
||||||
|
const [vevent, vtimezone] = result[2];
|
||||||
|
expect(vevent[0]).toBe("vevent");
|
||||||
|
expect(vtimezone[0]).toBe("vtimezone");
|
||||||
|
|
||||||
|
// Should use UTC timezone as fallback
|
||||||
|
expect(vtimezone[2]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
[
|
||||||
|
"standard",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzname", {}, "text", "UTC"],
|
||||||
|
["dtstart", {}, "date-time", "1970-01-01T00:00:00"],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty string timezone with UTC fallback", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
uid: "event-empty-tz",
|
||||||
|
title: "Empty Timezone Event",
|
||||||
|
start: new Date("2025-07-23T10:00:00"),
|
||||||
|
end: new Date("2025-07-23T11:00:00"),
|
||||||
|
timezone: "",
|
||||||
|
allday: false,
|
||||||
|
attendee: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = calendarEventToJCal(mockEvent as CalendarEvent);
|
||||||
|
|
||||||
|
expect(result[0]).toBe("vcalendar");
|
||||||
|
const [vevent, vtimezone] = result[2];
|
||||||
|
expect(vevent[0]).toBe("vevent");
|
||||||
|
expect(vtimezone[0]).toBe("vtimezone");
|
||||||
|
|
||||||
|
// Should use UTC timezone as fallback
|
||||||
|
expect(vtimezone[2]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
[
|
||||||
|
"standard",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+00:00"],
|
||||||
|
["tzname", {}, "text", "UTC"],
|
||||||
|
["dtstart", {}, "date-time", "1970-01-01T00:00:00"],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles valid timezone correctly", () => {
|
||||||
|
const mockEvent = {
|
||||||
|
uid: "event-valid-tz",
|
||||||
|
title: "Valid Timezone Event",
|
||||||
|
start: new Date("2025-07-23T10:00:00"),
|
||||||
|
end: new Date("2025-07-23T11:00:00"),
|
||||||
|
timezone: "Europe/Paris",
|
||||||
|
allday: false,
|
||||||
|
attendee: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = calendarEventToJCal(mockEvent as CalendarEvent);
|
||||||
|
|
||||||
|
expect(result[0]).toBe("vcalendar");
|
||||||
|
const [vevent, vtimezone] = result[2];
|
||||||
|
expect(vevent[0]).toBe("vevent");
|
||||||
|
expect(vtimezone[0]).toBe("vtimezone");
|
||||||
|
|
||||||
|
// Should use the specified timezone - check for Europe/Paris timezone components
|
||||||
|
expect(vtimezone[2]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
[
|
||||||
|
"daylight",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+01:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+02:00"],
|
||||||
|
["tzname", {}, "text", "CEST"],
|
||||||
|
["dtstart", {}, "date-time", "1970-03-29T02:00:00"],
|
||||||
|
[
|
||||||
|
"rrule",
|
||||||
|
{},
|
||||||
|
"recur",
|
||||||
|
{ byday: "-1SU", bymonth: 3, freq: "YEARLY" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"standard",
|
||||||
|
[
|
||||||
|
["tzoffsetfrom", {}, "utc-offset", "+02:00"],
|
||||||
|
["tzoffsetto", {}, "utc-offset", "+01:00"],
|
||||||
|
["tzname", {}, "text", "CET"],
|
||||||
|
["dtstart", {}, "date-time", "1970-10-25T03:00:00"],
|
||||||
|
[
|
||||||
|
"rrule",
|
||||||
|
{},
|
||||||
|
"recur",
|
||||||
|
{ byday: "-1SU", bymonth: 10, freq: "YEARLY" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
main {
|
.App {
|
||||||
|
background-color: #f3f6f9;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.main-layout {
|
||||||
|
background-color: #fff;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
height: calc(100vh - 90px);
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar {
|
.calendar {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: "hidden";
|
height: 100%;
|
||||||
height: 90vh;
|
overflow: hidden;
|
||||||
|
padding: 0 25px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendarListHeader {
|
.calendarListHeader {
|
||||||
@@ -16,38 +25,22 @@ main {
|
|||||||
margin-left: 25%;
|
margin-left: 25%;
|
||||||
}
|
}
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 2px gray;
|
border-right: 2px solid #f3f4f6;
|
||||||
width: 20vw;
|
width: 20vw;
|
||||||
|
padding: 16px 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-daygrid-day {
|
|
||||||
min-height: 10px !important; /* Default is ~40px */
|
|
||||||
height: 10px !important;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
.fc .fc-timegrid-slot {
|
|
||||||
height: 60px !important; /* Default is ~40px */
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-timegrid-slot {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.declined-event {
|
.declined-event {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tentative-event {
|
|
||||||
}
|
|
||||||
|
|
||||||
.needs-action-event {
|
.needs-action-event {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
border: dashed 1px #ffffff;
|
border: dashed 1px #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-slot {
|
.fc .fc-timegrid-slot {
|
||||||
height: 30px !important;
|
height: auto;
|
||||||
min-height: 30px !important;
|
min-height: 54px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-slot-label {
|
.fc .fc-timegrid-slot-label {
|
||||||
@@ -74,14 +67,6 @@ main {
|
|||||||
overflow: hidden; /* Important: let FullCalendar handle internal scroll */
|
overflow: hidden; /* Important: let FullCalendar handle internal scroll */
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-scroller-liquid-absolute {
|
|
||||||
overflow-y: auto !important; /* This targets the scrollable part */
|
|
||||||
max-height: calc(100vh - 100px); /* Adjust based on your header size */
|
|
||||||
}
|
|
||||||
.scrollgrid-section-header {
|
|
||||||
border: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base container */
|
/* Base container */
|
||||||
.fc-daygrid-day-top {
|
.fc-daygrid-day-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -91,36 +76,6 @@ main {
|
|||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Day number */
|
|
||||||
.fc-daygrid-day-top .fc-daygrid-day-number {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Day name (e.g., MON) */
|
|
||||||
.fc-daygrid-day-top small {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: #94a3b8; /* slate gray */
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Today’s date highlight */
|
|
||||||
.current-date {
|
|
||||||
background-color: orange;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc .fc-col-header-cell,
|
|
||||||
.fc .fc-scrollgrid table,
|
|
||||||
.fc-theme-standard .fc-scrollgrid {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sidebar*/
|
/* sidebar*/
|
||||||
.sidebar-calendar {
|
.sidebar-calendar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
@@ -207,3 +162,112 @@ main {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export default function CalendarApp() {
|
|||||||
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
const pending = useAppSelector((state) => state.calendars.pending);
|
const pending = useAppSelector((state) => state.calendars.pending);
|
||||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
const userId =
|
||||||
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const selectPersonnalCalendars = createSelector(
|
const selectPersonnalCalendars = createSelector(
|
||||||
(state) => state.calendars,
|
(state) => state.calendars,
|
||||||
(calendars) =>
|
(calendars) =>
|
||||||
@@ -80,9 +81,21 @@ export default function CalendarApp() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [selectedCalendars, setSelectedCalendars] = useState<string[]>(
|
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
||||||
Object.keys(calendars).filter((id) => id.split("/")[0] === userId)
|
|
||||||
);
|
// Auto-select personal calendars when first loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
Object.keys(calendars).length > 0 &&
|
||||||
|
userId &&
|
||||||
|
selectedCalendars.length === 0
|
||||||
|
) {
|
||||||
|
const personalCalendarIds = Object.keys(calendars).filter(
|
||||||
|
(id) => id.split("/")[0] === userId
|
||||||
|
);
|
||||||
|
setSelectedCalendars(personalCalendarIds);
|
||||||
|
}
|
||||||
|
}, [calendars, userId, selectedCalendars.length]);
|
||||||
|
|
||||||
const calendarRange = getCalendarRange(selectedDate);
|
const calendarRange = getCalendarRange(selectedDate);
|
||||||
|
|
||||||
@@ -163,7 +176,7 @@ export default function CalendarApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="main-layout">
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<div className="calendar-label">
|
<div className="calendar-label">
|
||||||
<div className="calendar-label">
|
<div className="calendar-label">
|
||||||
@@ -304,8 +317,10 @@ export default function CalendarApp() {
|
|||||||
weekNumbers
|
weekNumbers
|
||||||
weekNumberFormat={{ week: "long" }}
|
weekNumberFormat={{ week: "long" }}
|
||||||
slotDuration={"00:30:00"}
|
slotDuration={"00:30:00"}
|
||||||
slotLabelInterval={"01:00:00"}
|
slotLabelInterval={"00:30:00"}
|
||||||
scrollTime={"08:00:00"}
|
scrollTime={new Date(Date.now() - 2 * 60 * 60 * 1000)
|
||||||
|
.toTimeString()
|
||||||
|
.slice(0, 5)}
|
||||||
unselectAuto={false}
|
unselectAuto={false}
|
||||||
allDayText=""
|
allDayText=""
|
||||||
slotLabelFormat={{
|
slotLabelFormat={{
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { Calendars } from "../../features/Calendars/CalendarTypes";
|
|||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Checkbox from "@mui/material/Checkbox";
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
|
||||||
function CalendarAccordion({
|
function CalendarAccordion({
|
||||||
@@ -34,8 +33,8 @@ function CalendarAccordion({
|
|||||||
}) {
|
}) {
|
||||||
const allCalendars = useAppSelector((state) => state.calendars.list);
|
const allCalendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
if (calendars.length === 0) return null;
|
|
||||||
const [expended, setExpended] = useState(defaultExpanded);
|
const [expended, setExpended] = useState(defaultExpanded);
|
||||||
|
if (calendars.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion defaultExpanded={defaultExpanded} expanded={expended}>
|
<Accordion defaultExpanded={defaultExpanded} expanded={expended}>
|
||||||
@@ -81,7 +80,8 @@ export default function CalendarSelection({
|
|||||||
selectedCalendars: string[];
|
selectedCalendars: string[];
|
||||||
setSelectedCalendars: Function;
|
setSelectedCalendars: Function;
|
||||||
}) {
|
}) {
|
||||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
const userId =
|
||||||
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
const personnalCalendars = Object.keys(calendars).filter(
|
const personnalCalendars = Object.keys(calendars).filter(
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: 7vh;
|
height: 80px;
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menubar-item {
|
.menubar-item {
|
||||||
|
|||||||
@@ -48,14 +48,18 @@ export function Menubar() {
|
|||||||
<Avatar
|
<Avatar
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: stringToColor(
|
bgcolor: stringToColor(
|
||||||
user.family_name ? user.family_name : user.email
|
user && user.family_name
|
||||||
|
? user.family_name
|
||||||
|
: user && user.email
|
||||||
|
? user.email
|
||||||
|
: ""
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
sizes="large"
|
sizes="large"
|
||||||
>
|
>
|
||||||
{user.name && user.family_name
|
{user?.name && user?.family_name
|
||||||
? `${user.name[0]}${user.family_name[0]}`
|
? `${user.name[0]}${user.family_name[0]}`
|
||||||
: user.email[0]}
|
: (user?.email?.[0] ?? "")}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function CalendarPopover({
|
|||||||
}) {
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const userId =
|
const userId =
|
||||||
useAppSelector((state) => state.user.userData.openpaasId) ?? "";
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const [name, setName] = useState(calendar?.name ?? "");
|
const [name, setName] = useState(calendar?.name ?? "");
|
||||||
const [description, setDescription] = useState(calendar?.description ?? "");
|
const [description, setDescription] = useState(calendar?.description ?? "");
|
||||||
const [color, setColor] = useState(calendar?.color ?? "");
|
const [color, setColor] = useState(calendar?.color ?? "");
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function EventDisplayModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const userPersonnalCalendars: Calendars[] = calendars.filter(
|
const userPersonnalCalendars: Calendars[] = calendars.filter(
|
||||||
(c) => c.id?.split("/")[0] === user.userData.openpaasId
|
(c) => c.id?.split("/")[0] === user.userData?.openpaasId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
@@ -97,7 +97,7 @@ export default function EventDisplayModal({
|
|||||||
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
||||||
const [newCalId, setNewCalId] = useState(event?.calId);
|
const [newCalId, setNewCalId] = useState(event?.calId);
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
calId.split("/")[0] === user.userData.openpaasId
|
calId.split("/")[0] === user.userData?.openpaasId
|
||||||
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
||||||
: calendars.findIndex((cal) => cal.id === calId)
|
: calendars.findIndex((cal) => cal.id === calId)
|
||||||
);
|
);
|
||||||
@@ -198,7 +198,7 @@ export default function EventDisplayModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calList =
|
const calList =
|
||||||
calId.split("/")[0] === user.userData.openpaasId
|
calId.split("/")[0] === user.userData?.openpaasId
|
||||||
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
||||||
<MenuItem key={index} value={index}>
|
<MenuItem key={index} value={index}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ function EventPopover({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const organizer = useAppSelector((state) => state.user.organiserData);
|
const organizer = useAppSelector((state) => state.user.organiserData);
|
||||||
const userId = useAppSelector((state) => state.user.userData.openpaasId);
|
const userId =
|
||||||
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const selectPersonnalCalendars = createSelector(
|
const selectPersonnalCalendars = createSelector(
|
||||||
(state) => state.calendars,
|
(state) => state.calendars,
|
||||||
(calendars) =>
|
(calendars) =>
|
||||||
|
|||||||
@@ -234,8 +234,17 @@ export function calendarEventToJCal(
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const timezoneData = TIMEZONES.zones[event.timezone];
|
||||||
|
if (!timezoneData) {
|
||||||
|
const vtimezone = new ICAL.Timezone({
|
||||||
|
component: TIMEZONES.zones["Etc/UTC"].ics,
|
||||||
|
tzid: "Etc/UTC",
|
||||||
|
});
|
||||||
|
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
||||||
|
}
|
||||||
|
|
||||||
const vtimezone = new ICAL.Timezone({
|
const vtimezone = new ICAL.Timezone({
|
||||||
component: TIMEZONES.zones[event.timezone].ics,
|
component: timezoneData.ics,
|
||||||
tzid: event.timezone,
|
tzid: event.timezone,
|
||||||
});
|
});
|
||||||
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
return ["vcalendar", [], [vevent, vtimezone.component.jCal]];
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useEffect } from "react";
|
|||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { Auth } from "./oidcAuth";
|
import { Auth } from "./oidcAuth";
|
||||||
import { Loading } from "../../components/Loading/Loading";
|
import { Loading } from "../../components/Loading/Loading";
|
||||||
import { Error } from "../../components/Error/Error";
|
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import { redirectTo } from "../../utils/apiUtils";
|
import { redirectTo } from "../../utils/apiUtils";
|
||||||
import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
|
import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
|
||||||
@@ -27,7 +26,7 @@ export function HandleLogin() {
|
|||||||
dispatch(setUserData(savedUser));
|
dispatch(setUserData(savedUser));
|
||||||
dispatch(getOpenPaasUserDataAsync());
|
dispatch(getOpenPaasUserDataAsync());
|
||||||
dispatch(getCalendarsListAsync());
|
dispatch(getCalendarsListAsync());
|
||||||
dispatch(push("/"));
|
dispatch(push("/calendar"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user