diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx
index 04ff24e..5f08660 100644
--- a/__test__/components/Calendar.test.tsx
+++ b/__test__/components/Calendar.test.tsx
@@ -40,7 +40,7 @@ describe("CalendarSelection", () => {
"user1/cal1": {
name: "Calendar personnal",
id: "user1/cal1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -72,7 +72,7 @@ describe("CalendarSelection", () => {
name: "Calendar delegated",
delegated: true,
id: "user2/cal1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -103,7 +103,7 @@ describe("CalendarSelection", () => {
"user3/cal1": {
name: "Calendar shared",
id: "user3/cal1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -292,7 +292,7 @@ describe("calendar Availability search", () => {
"user1/cal1": {
name: "Calendar personnal",
id: "user1/cal1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {},
},
diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx
index e0c7717..185f698 100644
--- a/__test__/components/EventModifications.test.tsx
+++ b/__test__/components/EventModifications.test.tsx
@@ -36,7 +36,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
- color: "#FF0000",
+ color: { light: "#FFFFFF", dark: "#000000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -123,7 +123,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
- color: "#FF0000",
+ color: { light: "#FFFFFF", dark: "#000000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
@@ -157,6 +157,10 @@ describe("CalendarApp integration", () => {
});
it("renders lock icon for private events on the calendar", async () => {
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 120,
+ });
const preloadedState = createPreloadedState({
class: "PRIVATE",
title: "Private Event",
@@ -166,15 +170,16 @@ describe("CalendarApp integration", () => {
,
preloadedState
);
-
- const eventEls = screen.getAllByText("Private Event");
- const found = eventEls.some((eventEl) =>
- within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
- );
- expect(found).toBe(true);
+ const card = screen.getByTestId("event-card-event1");
+ const lockIcon = within(card).getByTestId("LockOutlineIcon");
+ expect(lockIcon).toBeInTheDocument();
});
it("renders lock icon for confidential events on the calendar", async () => {
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 120,
+ });
const preloadedState = createPreloadedState({
class: "CONFIDENTIAL",
title: "Confidential Event",
@@ -185,14 +190,16 @@ describe("CalendarApp integration", () => {
preloadedState
);
- const eventEls = screen.getAllByText("Confidential Event");
- const found = eventEls.some((eventEl) =>
- within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
- );
- expect(found).toBe(true);
+ const card = screen.getByTestId("event-card-event1");
+ const lockIcon = within(card).getByTestId("LockOutlineIcon");
+ expect(lockIcon).toBeInTheDocument();
});
it("does NOT render a lock icon for public events on the calendar", async () => {
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ value: 120,
+ });
const preloadedState = createPreloadedState({
class: "PUBLIC",
title: "Public Event",
@@ -203,11 +210,9 @@ describe("CalendarApp integration", () => {
preloadedState
);
- const eventEls = screen.getAllByText("Public Event");
- const found = eventEls.some((eventEl) =>
- within(eventEl.parentElement as HTMLElement).queryByTestId("lock-icon")
- );
- expect(found).toBe(false);
+ const card = screen.getByTestId("event-card-event1");
+ const lockIcon = within(card).queryByTestId("LockOutlineIcon");
+ expect(lockIcon).not.toBeInTheDocument();
});
it("does render a title for events without any attendees or user as organizer", async () => {
@@ -229,7 +234,7 @@ describe("CalendarApp integration", () => {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["alice@example.com"],
events: {
event1: {
diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx
index 34ed2f1..f2e478d 100644
--- a/__test__/components/MiniCalendarColor.test.tsx
+++ b/__test__/components/MiniCalendarColor.test.tsx
@@ -28,7 +28,7 @@ describe("MiniCalendar", () => {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
- color: "#FF0000",
+ color: { light: "#FF0000", dark: "#000" },
ownerEmails: ["test@test.com"],
events: {
event1: {
diff --git a/src/components/Calendar/Calendar.styl b/src/components/Calendar/Calendar.styl
index 99b73df..8e251eb 100644
--- a/src/components/Calendar/Calendar.styl
+++ b/src/components/Calendar/Calendar.styl
@@ -50,19 +50,12 @@
.sidebar:hover
scrollbar-color: rgba(0, 0, 0, 0.5) transparent
-.declined-event
- opacity 0.7
+// .declined-event
+// opacity 0.7
-.needs-action-event
- opacity 0.7
- border dashed 1px #ffffff
-
-.fc-event-main span,
-.fc-daygrid-event div
- display block !important
- white-space nowrap !important
- overflow hidden !important
- text-overflow ellipsis !important
+// .needs-action-event
+// opacity 0.7
+// border dashed 1px #ffffff
[data-theme="dark"] .fc-timegrid-slot::after
border-top 1px dotted rgba(255, 255, 255, 0.2)
diff --git a/src/components/Calendar/CustomCalendar.styl b/src/components/Calendar/CustomCalendar.styl
index 16dcdf4..0a30d73 100644
--- a/src/components/Calendar/CustomCalendar.styl
+++ b/src/components/Calendar/CustomCalendar.styl
@@ -251,37 +251,26 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
/* FullCalendar "more" popover restyle */
-.fc-daygrid-day-events
- display: flex
- flex-direction: column
- height 70%
-
-.fc-daygrid-day-bottom
- position absolute
- bottom 0
- font-weight 500
- font-size 12px
- line-height 16px
- letter-spacing 0.5px
- color #6D7885
-
.fc-more-popover
z-index 700 !important
- border-radius 4px
+ border-radius 8px
background #fff
overflow hidden
- font-family Inter
+ font-family Inter, sans-serif
width 244px
+ box-shadow 0 4px 16px rgba(0,0,0,0.1)
.fc-popover-header
background #FCFCFC
+ display flex
justify-content space-between
+ align-items center
width 244px
height 40px
padding 12px
+ border-bottom 1px solid #eee
.fc-popover-title
- font-family Inter
font-weight 500
font-size 14px
color #243B55
@@ -290,36 +279,66 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
.fc-popover-close
color #605D62
+ cursor pointer
.fc-popover-body
max-height 208px
overflow auto
padding 0 !important
+ background #fff
+ .MuiCard-root
+ box-shadow none !important
+ border none !important
+ border-radius 8px
+ background-color transparent !important
+ margin 2px 6px
+ transition background 0.15s ease
+
+
+ &:hover
+ background #f7f7f8
+
+ .MuiCardHeader-root
+ padding 4px 8px
+ .MuiTypography-root
+ white-space nowrap
+ overflow hidden
+ text-overflow ellipsis
+ font-family: Inter !important
+ font-weight: 400 !important
+ font-style: Regular !important
+ font-size: 14px !important
+ line-height: 20px !important
+ letter-spacing: 0.25px !important
+ vertical-align: middle !important
+ color #1C1B1F !important
+ .compactStartTime
+ display none
+
+ .MuiCardContent-root
+ padding 0 8px 4px 8px
+ font-size 12px
+ color #6D7885
+ line-height 16px
+
+ /* Hide FullCalendar’s extra margin block */
.fc-more-popover-misc
height 0 !important
margin 0 !important
padding 0 !important
- .fc-daygrid-event
- &:hover
- background #f7f7f8
- div
- display flex
- align-items center
- padding 4px 6px 4px 12px
- border-radius 8px
- color #1C1B1F!important
- background-color transparent!important
- border none
- color #334155
- font-size 14px
- font-weight 400
- line-height 20px
- letter-spacing 0.25px
- vertical-align middle
- transition background 0.15s ease, color 0.15s ease
- span
- white-space nowrap
- overflow hidden
- text-overflow ellipsis
+// Custom event chips
+.fc-event
+ background-color transparent
+ border none
+ width 95%
+ height 100%
+ align-content left
+ .fc-event-main
+ display flex
+ flex-direction row
+ align-items center
+
+.fc-timegrid-event-harness-inset .fc-timegrid-event
+ box-shadow none
diff --git a/src/components/Calendar/handlers/viewHandlers.ts b/src/components/Calendar/handlers/viewHandlers.ts
index 94e4948..1003a8b 100644
--- a/src/components/Calendar/handlers/viewHandlers.ts
+++ b/src/components/Calendar/handlers/viewHandlers.ts
@@ -1,13 +1,10 @@
import React from "react";
import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core";
-import { updateSlotLabelVisibility } from "../utils/calendarUtils";
import { createMouseHandlers } from "./mouseHandlers";
import { userAttendee } from "../../../features/User/userDataTypes";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
-import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
-import AccessTimeIcon from "@mui/icons-material/AccessTime";
-import LockIcon from "@mui/icons-material/Lock";
import { EventErrorHandler } from "../../Error/EventErrorHandler";
+import { EventChip } from "../../Event/EventChip/EventChip";
export interface ViewHandlersProps {
calendarRef: React.RefObject;
@@ -108,79 +105,12 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
};
const handleEventContent = (arg: any) => {
- const event = arg.event;
- const props = event._def.extendedProps;
- const {
- calId,
- temp,
- attendee: attendees = [],
- class: classification,
- } = props;
- try {
- const calendarsSource = temp ? tempcalendars : calendars;
- const calendar = calendarsSource[calId];
- if (!calendar) return null;
-
- const isPrivate = ["PRIVATE", "CONFIDENTIAL"].includes(classification);
- const ownerEmails = new Set(
- calendar.ownerEmails?.map((e) => e.toLowerCase())
- );
- const delegated = calendar.delegated;
- let Icon = null;
- let titleStyle: React.CSSProperties = {
- color: event._def.extendedProps.colors?.dark,
- };
-
- const showSpecialDisplay = attendees.filter((att: userAttendee) =>
- ownerEmails.has(att.cal_address.toLowerCase())
- );
- // if no special display
- if (attendees.length && !delegated && !showSpecialDisplay.length) {
- return React.createElement(
- "div",
- { style: { display: "flex", alignItems: "center" } },
- React.createElement("span", null, event.title)
- );
- }
- switch (showSpecialDisplay?.[0]?.partstat) {
- case "DECLINED":
- Icon = null;
- titleStyle.textDecoration = "line-through";
- break;
- case "TENTATIVE":
- Icon = HelpOutlineIcon;
- break;
- case "NEEDS-ACTION":
- Icon = AccessTimeIcon;
- break;
- case "ACCEPTED":
- Icon = null;
- break;
- default:
- break;
- }
-
- return RenderEventTitle(
- titleStyle,
- isPrivate,
- Icon,
- arg,
- event,
- calendar
- );
- } catch (e) {
- const message =
- e instanceof Error ? e.message : "Unknown error during rendering";
- errorHandler.reportError(
- event._def.extendedProps.uid || event.id,
- message
- );
- return React.createElement(
- "div",
- { style: { display: "flex", alignItems: "center" } },
- React.createElement("span", null, event.title)
- );
- }
+ return React.createElement(EventChip, {
+ arg,
+ calendars,
+ tempcalendars,
+ errorHandler,
+ });
};
const handleEventDidMount = (arg: any) => {
@@ -228,65 +158,3 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
handleEventDidMount,
};
};
-
-function RenderEventTitle(
- titleStyle: React.CSSProperties,
- isPrivate: boolean,
- Icon: any,
- arg: any,
- event: any,
- calendar: Calendars
-): React.ReactNode {
- const isMonthView = arg.view.type === "dayGridMonth";
- const startTime = event._instance.range.start.toLocaleTimeString(undefined, {
- hour: "2-digit",
- minute: "2-digit",
- });
-
- const icons: React.ReactNode[] = [];
- if (isPrivate) {
- icons.push(
- React.createElement(LockIcon, {
- "data-testid": "lock-icon",
- fontSize: "small",
- style: { marginRight: "4px" },
- })
- );
- }
- if (Icon) {
- icons.push(
- React.createElement(Icon, {
- fontSize: "small",
- style: { marginRight: "4px" },
- })
- );
- }
-
- const containerStyle: React.CSSProperties = isMonthView
- ? {
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: calendar.color?.light,
- color: "white",
- borderRadius: "4px",
- border: "1px",
- width: "100%",
- }
- : {
- display: "flex",
- alignItems: "center",
- };
-
- const contentText =
- isMonthView && !event._def.extendedProps.allday
- ? `${startTime} ${event.title}`
- : event.title;
-
- return React.createElement(
- "div",
- { style: containerStyle },
- ...icons,
- React.createElement("span", { style: titleStyle }, contentText)
- );
-}
diff --git a/src/components/Calendar/utils/calendarUtils.ts b/src/components/Calendar/utils/calendarUtils.ts
index 541b5fd..e0d1ddc 100644
--- a/src/components/Calendar/utils/calendarUtils.ts
+++ b/src/components/Calendar/utils/calendarUtils.ts
@@ -62,9 +62,9 @@ export const eventToFullCalendarFormat = (
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
.map((e) => {
if (e.calId.split("/")[0] === userId) {
- return { ...e, colors: e.color, color: e.color?.light, editable: true };
+ return { ...e, colors: e.color, editable: true };
}
- return { ...e, colors: e.color, color: e.color?.light, editable: false };
+ return { ...e, colors: e.color, editable: false };
});
};
diff --git a/src/components/Event/EventChip/ErrorEventChip.tsx b/src/components/Event/EventChip/ErrorEventChip.tsx
new file mode 100644
index 0000000..58219f7
--- /dev/null
+++ b/src/components/Event/EventChip/ErrorEventChip.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { Card, Typography } from "@mui/material";
+import { EventErrorHandler } from "../../Error/EventErrorHandler";
+
+export function ErrorEventChip({
+ event,
+ errorHandler,
+ error,
+}: {
+ event: any;
+ errorHandler: EventErrorHandler;
+ error: any;
+}) {
+ const message =
+ error instanceof Error ? error.message : "Unknown error during rendering";
+ errorHandler.reportError(event._def.extendedProps.uid || event.id, message);
+
+ return (
+
+ {event.title}
+
+ );
+}
diff --git a/src/components/Event/EventChip/EventChip.tsx b/src/components/Event/EventChip/EventChip.tsx
new file mode 100644
index 0000000..41e82d6
--- /dev/null
+++ b/src/components/Event/EventChip/EventChip.tsx
@@ -0,0 +1,316 @@
+import {
+ Avatar,
+ Box,
+ Card,
+ CardContent,
+ CardHeader,
+ Typography,
+} from "@mui/material";
+import React, { useEffect, useRef, useState } from "react";
+import { stringAvatar } from "../utils/eventUtils";
+import { ErrorEventChip } from "./ErrorEventChip";
+import {
+ DisplayedIcons,
+ EventChipProps,
+ getBestColor,
+ getCardStyle,
+ getEventDuration,
+ getEventTimes,
+ getOwnerAttendee,
+ getTitleStyle,
+ IconDisplayConfig,
+} from "./EventChipUtils";
+import { SimpleEventChip } from "./SimpleEventChip";
+
+const COMPACT_WIDTH_THRESHOLD = 100;
+const PRIVATE_CLASSIFICATIONS = ["PRIVATE", "CONFIDENTIAL"];
+
+export const EVENT_DURATION = {
+ SHORT: 15,
+ MEDIUM: 30,
+ LONG: 60,
+} as const;
+
+function useCompactMode(
+ cardRef: React.RefObject
+): boolean {
+ const [showCompact, setShowCompact] = useState(false);
+
+ useEffect(() => {
+ const checkWidth = () => {
+ if (cardRef.current) {
+ const width = cardRef.current.offsetWidth;
+ setShowCompact(width < COMPACT_WIDTH_THRESHOLD);
+ }
+ };
+
+ checkWidth();
+
+ const resizeObserver = new ResizeObserver(checkWidth);
+ if (cardRef.current) {
+ resizeObserver.observe(cardRef.current);
+ }
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, []);
+
+ return showCompact;
+}
+
+export function EventChip({
+ arg,
+ calendars,
+ tempcalendars,
+ errorHandler,
+}: EventChipProps) {
+ const cardRef = useRef(null);
+ const showCompact = useCompactMode(cardRef);
+
+ const event = arg.event;
+ const props = event._def.extendedProps;
+ const {
+ calId,
+ temp,
+ attendee: attendees = [],
+ class: classification,
+ } = props;
+
+ try {
+ // Calendar validation
+ const calendarsSource = temp ? tempcalendars : calendars;
+ const calendar = calendarsSource[calId];
+ if (!calendar) return null;
+
+ // Event properties
+ const isPrivate = PRIVATE_CLASSIFICATIONS.includes(classification);
+ const ownerEmails = new Set(
+ calendar.ownerEmails?.map((e) => e.toLowerCase())
+ );
+ const delegated = calendar.delegated;
+
+ // Determine owner attendee
+ const ownerAttendee = getOwnerAttendee(attendees, ownerEmails);
+
+ // Handle non-owner attendee case
+ if (attendees.length && !delegated && !ownerAttendee) {
+ return ;
+ }
+
+ // Color and contrast logic
+ const bestColor = getBestColor(
+ (calendar.color as { light: string; dark: string }) ?? {
+ light: "#fff",
+ dark: "#000",
+ }
+ );
+
+ // Icon display configuration
+ const IconDisplayed: IconDisplayConfig = {
+ declined: ownerAttendee?.partstat === "DECLINED",
+ tentative: ownerAttendee?.partstat === "TENTATIVE",
+ needAction: ownerAttendee?.partstat === "NEEDS-ACTION",
+ private: isPrivate,
+ };
+
+ // View and time calculations
+ const isMonthView = arg.view.type === "dayGridMonth";
+ const timeZone = arg.view.calendar?.getOption("timeZone") || "UTC";
+ const { startTime, endTime } = getEventTimes(event, timeZone);
+ const eventLength = getEventDuration(event);
+
+ const isMoreThan15 = eventLength > EVENT_DURATION.SHORT;
+ const isMoreThan30 = eventLength > EVENT_DURATION.MEDIUM;
+ const isMoreThan60 = eventLength > EVENT_DURATION.LONG;
+
+ // Style calculation
+ const titleStyle = getTitleStyle(
+ bestColor,
+ ownerAttendee?.partstat,
+ calendar
+ );
+ const cardStyle = getCardStyle(
+ bestColor,
+ eventLength,
+ ownerAttendee?.partstat,
+ calendar
+ );
+
+ // Organizer avatar
+ const OrganizerAvatar = event._def.extendedProps.organizer
+ ? stringAvatar(
+ event._def.extendedProps.organizer?.cn ??
+ event._def.extendedProps.organizer?.cal_address
+ )
+ : { style: {}, children: null };
+
+ return (
+
+
+ {event.title}
+
+ ) : (
+
+
+ {(!isMoreThan30 || isMonthView) &&
+ !event._def.extendedProps.allday && (
+
+ {startTime}
+
+ )}
+ {DisplayedIcons(IconDisplayed)}
+
+ {event.title}
+
+
+
+ )
+ }
+ subheader={
+ isMoreThan30 &&
+ !isMonthView &&
+ !event._def.extendedProps.allday && (
+
+ {startTime} {!showCompact && ` - ${endTime}`}
+
+ )
+ }
+ />
+ {isMoreThan60 &&
+ !showCompact &&
+ !isMonthView &&
+ !event._def.extendedProps.allday && (
+
+ {event._def.extendedProps.location && (
+
+ {event._def.extendedProps.location}
+
+ )}
+
+ {event._def.extendedProps.description && (
+
+ {event._def.extendedProps.description}
+
+ )}
+
+
+ )}
+ {(isMoreThan60 || eventLength === 60) &&
+ !isMonthView &&
+ !event._def.extendedProps.allday &&
+ event._def.extendedProps.organizer &&
+ !showCompact &&
+ (window as any).displayOrgAvatar && (
+
+ )}
+
+ );
+ } catch (e) {
+ return (
+
+ );
+ }
+}
diff --git a/src/components/Event/EventChip/EventChipUtils.tsx b/src/components/Event/EventChip/EventChipUtils.tsx
new file mode 100644
index 0000000..c6071e7
--- /dev/null
+++ b/src/components/Event/EventChip/EventChipUtils.tsx
@@ -0,0 +1,182 @@
+import CancelIcon from "@mui/icons-material/Cancel";
+import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
+import LockOutlineIcon from "@mui/icons-material/LockOutline";
+import { Box, getContrastRatio } from "@mui/material";
+import moment from "moment";
+import React from "react";
+import { Calendars } from "../../../features/Calendars/CalendarTypes";
+import { userAttendee } from "../../../features/User/userDataTypes";
+import { EventErrorHandler } from "../../Error/EventErrorHandler";
+import { EVENT_DURATION } from "./EventChip";
+
+export interface EventChipProps {
+ arg: any;
+ calendars: Record;
+ tempcalendars: Record;
+ errorHandler: EventErrorHandler;
+}
+export interface IconDisplayConfig {
+ declined: boolean;
+ tentative: boolean;
+ needAction: boolean;
+ private: boolean;
+}
+export function getEventDuration(event: any): number {
+ return moment(event._instance.range.end).diff(
+ moment(event._instance.range.start),
+ "minutes"
+ );
+}
+export function getBestColor(colors: { light: string; dark: string }): string {
+ const contrastToDark = getContrastRatio(colors?.dark, "#fff");
+ const contrastToLight = getContrastRatio(colors?.light, "#fff");
+ return contrastToDark > contrastToLight ? colors?.dark : colors?.light;
+}
+export function getEventTimes(event: any, timeZone: string) {
+ return {
+ startTime: moment.tz(event.start, timeZone).format("HH:mm"),
+ endTime: moment.tz(event.end, timeZone).format("HH:mm"),
+ };
+}
+export function getOwnerAttendee(
+ attendees: userAttendee[],
+ ownerEmails: Set
+): userAttendee | undefined {
+ return attendees.find((att) =>
+ ownerEmails.has(att.cal_address.toLowerCase())
+ );
+}
+
+export function getTitleStyle(
+ bestColor: string,
+ partstat?: string,
+ calendar?: any
+): React.CSSProperties {
+ const baseStyle: React.CSSProperties = {
+ fontFamily: "Roboto",
+ fontWeight: "500",
+ fontStyle: "Medium",
+ fontSize: "12px",
+ lineHeight: "16px",
+ letterSpacing: "0.5px",
+ verticalAlign: "middle",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ font: "Roboto",
+ whiteSpace: "nowrap",
+ color: bestColor,
+ };
+
+ switch (partstat) {
+ case "DECLINED":
+ return { ...baseStyle, textDecoration: "line-through" };
+ case "TENTATIVE":
+ case "ACCEPTED":
+ return { ...baseStyle, color: calendar?.color?.dark };
+ case "NEEDS-ACTION":
+ return baseStyle;
+ default:
+ return baseStyle;
+ }
+}
+
+function getEventVariant(duration: number) {
+ if (duration <= EVENT_DURATION.SHORT) return "short";
+ if (duration <= EVENT_DURATION.MEDIUM) return "medium";
+ if (duration <= EVENT_DURATION.LONG) return "long";
+ return "extraLong";
+}
+
+function getCardVariantStyle(
+ variant: string,
+ baseColor: string
+): React.CSSProperties {
+ const shared: React.CSSProperties = {
+ width: "100%",
+ height: "100%",
+ borderRadius: "8px",
+ boxShadow: "none",
+ border: `1px solid ${baseColor}`,
+ color: baseColor,
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "flex-start",
+ };
+
+ switch (variant) {
+ case "short":
+ return { ...shared, padding: "0px 6px", justifyContent: "center" };
+ case "medium":
+ return { ...shared, padding: "4px 6px", justifyContent: "center" };
+ default:
+ return {
+ ...shared,
+ padding: "4px 6px",
+ };
+ }
+}
+
+export function getCardStyle(
+ bestColor: string,
+ eventLength: number,
+ partstat?: string,
+ calendar?: any
+): React.CSSProperties {
+ const baseStyle: React.CSSProperties = getCardVariantStyle(
+ getEventVariant(eventLength),
+ bestColor
+ );
+
+ switch (partstat) {
+ case "DECLINED":
+ return baseStyle;
+ case "TENTATIVE":
+ return {
+ ...baseStyle,
+ backgroundColor: calendar?.color?.light,
+ color: calendar?.color?.dark,
+ border: "1px solid white",
+ };
+ case "NEEDS-ACTION":
+ return {
+ ...baseStyle,
+ backgroundColor: "#fff",
+ };
+ case "ACCEPTED":
+ return {
+ ...baseStyle,
+ backgroundColor: calendar?.color?.light,
+ color: calendar?.color?.dark,
+ border: "1px solid white",
+ };
+ default:
+ return baseStyle;
+ }
+}
+
+export function DisplayedIcons(IconDisplayed: IconDisplayConfig) {
+ if (!Object.values(IconDisplayed).find((b) => b === true)) return;
+ return (
+
+ {IconDisplayed.needAction && (
+
+ )}
+ {IconDisplayed.declined && (
+
+ )}
+ {IconDisplayed.tentative && (
+
+ )}
+ {IconDisplayed.private && (
+
+ )}
+
+ );
+}
diff --git a/src/components/Event/EventChip/SimpleEventChip.tsx b/src/components/Event/EventChip/SimpleEventChip.tsx
new file mode 100644
index 0000000..a28d29d
--- /dev/null
+++ b/src/components/Event/EventChip/SimpleEventChip.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { Card, Typography } from "@mui/material";
+
+export function SimpleEventChip({ title }: { title: string }) {
+ return (
+
+
+ {title}
+
+
+ );
+}
diff --git a/src/components/Event/InfoRow.tsx b/src/components/Event/InfoRow.tsx
index 5695f1b..cb5388a 100644
--- a/src/components/Event/InfoRow.tsx
+++ b/src/components/Event/InfoRow.tsx
@@ -7,6 +7,7 @@ type InfoRowProps = {
error?: boolean;
data?: string; // optional link target
content?: React.ReactNode; // if provided, overrides text rendering
+ style?: React.CSSProperties;
};
function detectUrls(text: string) {
@@ -60,6 +61,7 @@ export function InfoRow({
error = false,
data,
content,
+ style,
}: InfoRowProps) {
return (
{data ? (
diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx
index 1abaf4a..4a3af92 100644
--- a/src/features/Events/EventDisplayPreview.tsx
+++ b/src/features/Events/EventDisplayPreview.tsx
@@ -564,6 +564,10 @@ export default function EventPreviewModal({
}
text={event.location}
+ style={{
+ fontSize: "16px",
+ fontFamily: "'Inter', sans-serif",
+ }}
/>
)}
{/* Description */}
@@ -575,6 +579,10 @@ export default function EventPreviewModal({
}
text={event.description}
+ style={{
+ fontSize: "16px",
+ fontFamily: "'Inter', sans-serif",
+ }}
/>
)}
{/* ALARM */}
@@ -586,6 +594,10 @@ export default function EventPreviewModal({
}
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
+ style={{
+ fontSize: "16px",
+ fontFamily: "'Inter', sans-serif",
+ }}
/>
)}
{/* Repetition */}
@@ -597,6 +609,10 @@ export default function EventPreviewModal({
}
text={makeRecurrenceString(event)}
+ style={{
+ fontSize: "16px",
+ fontFamily: "'Inter', sans-serif",
+ }}
/>
)}
>
@@ -633,6 +649,10 @@ export default function EventPreviewModal({
}
text={event.error}
+ style={{
+ fontSize: "16px",
+ fontFamily: "'Inter', sans-serif",
+ }}
error
/>
)}
diff --git a/src/setupTests.ts b/src/setupTests.ts
index f55b261..5a47074 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -43,6 +43,14 @@ jest.mock("openid-client", () => ({
}));
const originalWarn = console.warn;
+class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+(global as any).ResizeObserver = ResizeObserverMock;
+
beforeAll(() => {
console.warn = (...args: unknown[]) => {
if (