"use client"; import { useMemo, useState } from "react"; import { ActionIcon, Box, Center, Group, Loader, Stack, Text, Tooltip, useMantineTheme } from "@mantine/core"; import { Calendar } from "@mantine/dates"; import { useElementSize } from "@mantine/hooks"; import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; import { getQueryKey } from "@trpc/react-query"; import dayjs from "dayjs"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useSettings } from "@homarr/settings"; import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client"; import actionTargetClasses from "../common/action-target.module.css"; import type { WidgetComponentProps } from "../definition"; import { getUsableWidgetQueryData } from "../common/query-state"; import { useWidgetRuntimeQueries } from "../runtime-hooks"; import { IntegrationErrorIndicator } from "../common/integration-error-indicator"; import { CalendarDay } from "./calender-day"; import { getCalendarAgendaEvents, groupEventsByDate, moveCalendarMonth, splitEvents, toCalendarDateKey, } from "./calendar-events"; import type { CalendarEventWithSource } from "./calendar-event-list"; import { CalendarEventList } from "./calendar-event-list"; import classes from "./component.module.css"; export default function CalendarWidget(props: WidgetComponentProps<"calendar">) { const [month, setMonth] = useState(() => new Date()); const runtimeInput = { integrationIds: props.integrationIds, month: month.getMonth(), year: month.getFullYear(), releaseType: props.options.releaseType, showUnmonitored: props.options.showUnmonitored, }; useWidgetRuntimeQueries( props.widgetRuntimeRef, props.integrationIds.length > 0 ? [getQueryKey(clientApi.widget.calendar.findAllEvents, runtimeInput, "query")] : [], ); 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, displayMode }: FetchCalendarProps) => { const input = { integrationIds, month: month.getMonth() + 1, year: month.getFullYear(), releaseType: options.releaseType, showUnmonitored: options.showUnmonitored, }; const calendarQuery = clientApi.widget.calendar.findAllEvents.useQuery(input); const data = getUsableWidgetQueryData(calendarQuery); const { isPending } = calendarQuery; const events = useMemo( () => data?.flatMap(({ events: integrationEvents, integration, error }) => error ? [] : integrationEvents.map( (event): CalendarEventWithSource => ({ ...event, source: { integrationId: integration.id, integrationName: integration.name }, }), ), ) ?? [], [data], ); const failedIntegrations = data?.flatMap(({ integration, error }) => error ? [{ integrationId: integration.id, integrationName: integration.name, error }] : [], ) ?? []; return ( ); }; interface CalendarBaseProps { isEditMode: boolean; events: CalendarEventWithSource[]; failedIntegrations: { integrationId: string; integrationName: string; error: string }[]; isPending: boolean; month: Date; setMonth: (date: Date) => void; options: WidgetComponentProps<"calendar">["options"]; displayMode?: WidgetComponentProps<"calendar">["displayMode"]; } const CalendarBase = ({ isEditMode, events, failedIntegrations, isPending, month, setMonth, options, displayMode, }: CalendarBaseProps) => { const locale = useCurrentIntlLocale(); 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( () => (displayMode === "advanced" ? [] : splitEvents(events)), [displayMode, events], ); const visibleEvents = useMemo( () => normalizedEvents.filter( (event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType), ), [normalizedEvents, options.releaseType], ); const eventsByDate = useMemo(() => groupEventsByDate(visibleEvents), [visibleEvents]); const agendaEvents = useMemo( () => (displayMode === "advanced" ? getCalendarAgendaEvents(events, month, options.releaseType) : []), [displayMode, events, month, options.releaseType], ); if (displayMode === "advanced") { return ( ); } return ( setMonth(new Date(previousMonth))} onNextMonth={(nextMonth) => setMonth(new Date(nextMonth))} highlightToday locale={locale} hideWeekdays={false} date={month} maxLevel="month" firstDayOfWeek={firstDayOfWeek} className={classes.calendar} w="100%" h="100%" styles={{ calendarHeaderControl: { 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", position: "relative", }, day: { borderRadius: actualItemRadius, width: "100%", height: "100%", position: "absolute", top: 0, left: 0, bottom: 0, right: 0, }, month: { height: "100%", }, weekday: { padding: 0, }, weekdaysRow: { height: 22, }, }} renderDay={(tileDate) => { const eventsForDate = eventsByDate.get(toCalendarDateKey(dayjs(tileDate).toDate())) ?? []; return ( ); }} /> {failedIntegrations.length > 0 && ( )} ); }; interface CalendarAgendaProps { events: CalendarEventWithSource[]; failedIntegrations: CalendarBaseProps["failedIntegrations"]; isPending: boolean; isEditMode: boolean; locale: string; month: Date; setMonth: (date: Date) => void; } const CalendarAgenda = ({ events, failedIntegrations, isPending, isEditMode, locale, month, setMonth, }: CalendarAgendaProps) => { const t = useI18n("widget.calendar.advanced"); const monthLabel = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(month); return ( setMonth(moveCalendarMonth(month, -1))} > {monthLabel} {isPending ? t("loading") : t("eventCount", { count: events.length })} setMonth(moveCalendarMonth(month, 1))} > {isPending ? (
) : events.length > 0 ? ( ) : ( {t("noEvents")} )}
); };