"use client"; import { useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { useMantineTheme } from "@mantine/core"; import { Calendar } from "@mantine/dates"; import { useElementSize } from "@mantine/hooks"; import dayjs from "dayjs"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import type { CalendarEvent } from "@homarr/integrations/types"; import { useSettings } from "@homarr/settings"; import type { WidgetComponentProps } from "../definition"; import { CalendarDay } from "./calender-day"; import classes from "./component.module.css"; export default function CalendarWidget(props: WidgetComponentProps<"calendar">) { const [month, setMonth] = useState(new Date()); if (props.integrationIds.length === 0) { return ; } return ; } interface FetchCalendarProps extends WidgetComponentProps<"calendar"> { month: Date; setMonth: (date: Date) => void; } const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => { const input = { integrationIds, month: month.getMonth(), year: month.getFullYear(), releaseType: options.releaseType, showUnmonitored: options.showUnmonitored, }; const [data] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(input, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, }); const utils = clientApi.useUtils(); clientApi.widget.calendar.subscribeToEvents.useSubscription(input, { onData(data) { utils.widget.calendar.findAllEvents.setData(input, (old) => { return old?.map((item) => { if (item.integration.id !== data.integration.id) return item; return { ...item, events: data.events, }; }); }); }, }); const events = useMemo(() => data.flatMap((item) => item.events), [data]); return ; }; interface CalendarBaseProps { isEditMode: boolean; events: CalendarEvent[]; month: Date; setMonth: (date: Date) => void; options: WidgetComponentProps<"calendar">["options"]; } const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => { const params = useParams(); const locale = params.locale as string; const { firstDayOfWeek } = useSettings(); const board = useRequiredBoard(); const mantineTheme = useMantineTheme(); const actualItemRadius = mantineTheme.radius[board.itemRadius]; const { ref, width, height } = useElementSize(); const isSmall = width < 256; const normalizedEvents = useMemo(() => splitEvents(events), [events]); return ( setMonth(new Date(month))} onNextMonth={(month) => setMonth(new Date(month))} highlightToday locale={locale} hideWeekdays={false} date={month} maxLevel="month" firstDayOfWeek={firstDayOfWeek} static={isEditMode} className={classes.calendar} w="100%" h="100%" ref={ref} styles={{ calendarHeaderControl: { pointerEvents: isEditMode ? "none" : undefined, borderRadius: "md", height: isSmall ? "1.5rem" : undefined, width: isSmall ? "1.5rem" : undefined, }, calendarHeaderLevel: { pointerEvents: "none", fontSize: isSmall ? "0.75rem" : undefined, height: "100%", }, levelsGroup: { height: "100%", padding: "md", }, calendarHeader: { maxWidth: "unset", marginBottom: 0, }, monthCell: { textAlign: "center", }, day: { borderRadius: actualItemRadius, width: "100%", height: "100%", position: "relative", }, month: { height: "100%", }, weekday: { padding: 0, }, }} renderDay={(tileDate) => { const eventsForDate = normalizedEvents .filter((event) => dayjs(event.startDate).isSame(tileDate, "day")) .filter( (event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType), ) .sort((eventA, eventB) => eventA.startDate.getTime() - eventB.startDate.getTime()); return ( ); }} /> ); }; /** * Splits multi-day events into multiple single-day events. * @param events The events to split. * @returns The split events. */ export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => { const splitEvents: CalendarEvent[] = []; for (const event of events) { if (!event.endDate) { splitEvents.push(event); continue; } if (dayjs(event.startDate).isSame(event.endDate, "day")) { splitEvents.push(event); continue; } if (dayjs(event.startDate).isAfter(event.endDate)) { // Invalid event, skip it continue; } // Event spans multiple days, split it let currentStart = dayjs(event.startDate); while (currentStart.isBefore(event.endDate)) { splitEvents.push({ ...event, startDate: currentStart.toDate(), endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(), }); currentStart = currentStart.add(1, "day").startOf("day"); } } return splitEvents; };