From ec7230fa94fa25234113fcfba9d63711b91cb242 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:50:22 +0100 Subject: [PATCH] [#189] added custom event chip (#262) Co-authored-by: Camille Moussu --- __test__/components/Calendar.test.tsx | 8 +- .../components/EventModifications.test.tsx | 43 +-- .../components/MiniCalendarColor.test.tsx | 2 +- src/components/Calendar/Calendar.styl | 17 +- src/components/Calendar/CustomCalendar.styl | 97 +++--- .../Calendar/handlers/viewHandlers.ts | 146 +------- .../Calendar/utils/calendarUtils.ts | 4 +- .../Event/EventChip/ErrorEventChip.tsx | 31 ++ src/components/Event/EventChip/EventChip.tsx | 316 ++++++++++++++++++ .../Event/EventChip/EventChipUtils.tsx | 182 ++++++++++ .../Event/EventChip/SimpleEventChip.tsx | 28 ++ src/components/Event/InfoRow.tsx | 5 +- src/features/Events/EventDisplayPreview.tsx | 20 ++ src/setupTests.ts | 8 + 14 files changed, 689 insertions(+), 218 deletions(-) create mode 100644 src/components/Event/EventChip/ErrorEventChip.tsx create mode 100644 src/components/Event/EventChip/EventChip.tsx create mode 100644 src/components/Event/EventChip/EventChipUtils.tsx create mode 100644 src/components/Event/EventChip/SimpleEventChip.tsx 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 (