From 95d66707f5d37424691b0b76302da9b5cc985bf7 Mon Sep 17 00:00:00 2001 From: Denis Papec Date: Sat, 25 Nov 2023 16:17:25 +0000 Subject: [PATCH] Feature: Implement iCal integration for calendar, improve styling (#2376) * Feature: Implement iCal integration, improve calendar/agenda styling * Delete calendar.jsx * Calendar proxy handler * code style * Add some basic error handling --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/calendar.md | 14 ++++- package-lock.json | 28 +++++++++ package.json | 1 + pnpm-lock.yaml | 20 +++++++ public/locales/en/common.json | 3 +- src/pages/_app.jsx | 5 +- src/utils/config/service-helpers.js | 2 + src/utils/contexts/calendar.jsx | 15 ----- src/widgets/calendar/agenda.jsx | 51 +++------------- src/widgets/calendar/component.jsx | 12 +++- src/widgets/calendar/event.jsx | 41 +++++++++++++ src/widgets/calendar/integrations/ical.jsx | 58 +++++++++++++++++++ src/widgets/calendar/integrations/lidarr.jsx | 6 +- src/widgets/calendar/integrations/radarr.jsx | 6 +- src/widgets/calendar/integrations/readarr.jsx | 6 +- src/widgets/calendar/integrations/sonarr.jsx | 6 +- src/widgets/calendar/monthly.jsx | 49 ++++++---------- src/widgets/calendar/proxy.js | 33 +++++++++++ src/widgets/calendar/widget.js | 8 +++ src/widgets/widgets.js | 2 + 20 files changed, 251 insertions(+), 115 deletions(-) delete mode 100644 src/utils/contexts/calendar.jsx create mode 100644 src/widgets/calendar/event.jsx create mode 100644 src/widgets/calendar/integrations/ical.jsx create mode 100644 src/widgets/calendar/proxy.js create mode 100644 src/widgets/calendar/widget.js diff --git a/docs/widgets/services/calendar.md b/docs/widgets/services/calendar.md index b1bc0ea47..f9ed62849 100644 --- a/docs/widgets/services/calendar.md +++ b/docs/widgets/services/calendar.md @@ -15,13 +15,20 @@ widget: firstDayInWeek: sunday # optional - defaults to monday view: monthly # optional - possible values monthly, agenda maxEvents: 10 # optional - defaults to 10 + showTime: true # optional - show time for event happening today - defaults to false integrations: # optional - - type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr + - type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr, ical service_group: Media # group name where widget exists service_name: Sonarr # service name for that widget color: teal # optional - defaults to pre-defined color for the service (teal for sonarr) params: # optional - additional params for the service unmonitored: true # optional - defaults to false, used with *arr stack + - type: ical # Show calendar events from another service + url: https://domain.url/with/link/to.ics # URL with calendar events + name: My Events # required - name for these calendar events + color: zinc # optional - defaults to pre-defined color for the service (zinc for ical) + params: # optional - additional params for the service + showName: true # optional - show name before event title in event line - defaults to false ``` ## Agenda @@ -33,6 +40,7 @@ widget: type: calendar view: agenda maxEvents: 10 # optional - defaults to 10 + showTime: true # optional - show time for event happening today - defaults to false previousDays: 3 # optional - shows events since three days ago - defaults to 0 integrations: # same as in Monthly view example ``` @@ -42,3 +50,7 @@ widget: Currently integrated widgets are [sonarr](sonarr.md), [radarr](radarr.md), [lidarr](lidarr.md) and [readarr](readarr.md). Supported colors can be found on [color palette](../../configs/settings.md#color-palette). + +### iCal + +This custom integration allows you to show events from any calendar that supports iCal format, for example, Google Calendar (go to `Settings`, select specific calendar, go to `Integrate calendar`, copy URL from `Public Address in iCal format`). diff --git a/package-lock.json b/package-lock.json index 16e2ed684..45c775797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@headlessui/react": "^1.7.2", "@kubernetes/client-node": "^0.17.1", + "cal-parser": "^1.0.2", "classnames": "^2.3.2", "compare-versions": "^5.0.1", "dockerode": "^3.3.4", @@ -1250,6 +1251,15 @@ "node": ">=14.16" } }, + "node_modules/cal-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cal-parser/-/cal-parser-1.0.2.tgz", + "integrity": "sha512-wlQwcF0fl4eLclyGdncF9rcNNq0ipRYZGagG6h3LVgRXvCWE1fdMUaCLXwfC9YWoz9jKKbjQAq7TpO2Y3yrvmA==", + "dependencies": { + "ical-date-parser": "^4.0.0", + "rrule": "^2.6.8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3382,6 +3392,11 @@ "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-1.2.0.tgz", "integrity": "sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg==" }, + "node_modules/ical-date-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ical-date-parser/-/ical-date-parser-4.0.0.tgz", + "integrity": "sha512-XRCK/FU1akC2ZaJOdKIeZI6BLLgzWUuE0pegSrrkEva89GOan5mNkLVqCU4EMhCJ9nkG5TLWdMXrVX1fNAkFzw==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5485,6 +5500,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrule": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.2.tgz", + "integrity": "sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/rrule/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 0b10fedf6..0688bf7d2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@headlessui/react": "^1.7.2", "@kubernetes/client-node": "^0.17.1", + "cal-parser": "^1.0.2", "classnames": "^2.3.2", "compare-versions": "^5.0.1", "dockerode": "^3.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335da832d..2d272b312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@kubernetes/client-node': specifier: ^0.17.1 version: 0.17.1 + cal-parser: + specifier: ^1.0.2 + version: 1.0.2 classnames: specifier: ^2.3.2 version: 2.3.2 @@ -864,6 +867,13 @@ packages: responselike: 3.0.0 dev: false + /cal-parser@1.0.2: + resolution: {integrity: sha512-wlQwcF0fl4eLclyGdncF9rcNNq0ipRYZGagG6h3LVgRXvCWE1fdMUaCLXwfC9YWoz9jKKbjQAq7TpO2Y3yrvmA==} + dependencies: + ical-date-parser: 4.0.0 + rrule: 2.7.2 + dev: false + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -2232,6 +2242,10 @@ packages: '@babel/runtime': 7.21.0 dev: false + /ical-date-parser@4.0.0: + resolution: {integrity: sha512-XRCK/FU1akC2ZaJOdKIeZI6BLLgzWUuE0pegSrrkEva89GOan5mNkLVqCU4EMhCJ9nkG5TLWdMXrVX1fNAkFzw==} + dev: false + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3538,6 +3552,12 @@ packages: dependencies: glob: 7.2.3 + /rrule@2.7.2: + resolution: {integrity: sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==} + dependencies: + tslib: 2.5.0 + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3a3f88f1d..cc78e7c6c 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -765,6 +765,7 @@ "inCinemas": "In cinemas", "physicalRelease": "Physical release", "digitalRelease": "Digital release", - "noEventsToday": "No events for today!" + "noEventsToday": "No events for today!", + "noEventsFound": "No events found" } } diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 25f843418..29c04da33 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -12,7 +12,6 @@ import { ColorProvider } from "utils/contexts/color"; import { ThemeProvider } from "utils/contexts/theme"; import { SettingsProvider } from "utils/contexts/settings"; import { TabProvider } from "utils/contexts/tab"; -import { EventProvider } from "utils/contexts/calendar"; function MyApp({ Component, pageProps }) { return ( @@ -32,9 +31,7 @@ function MyApp({ Component, pageProps }) { - - - + diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index b054fb960..c2319781c 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -351,6 +351,7 @@ export function cleanServiceGroups(groups) { firstDayInWeek, integrations, maxEvents, + showTime, previousDays, view, @@ -519,6 +520,7 @@ export function cleanServiceGroups(groups) { if (view) cleanedService.widget.view = view; if (maxEvents) cleanedService.widget.maxEvents = maxEvents; if (previousDays) cleanedService.widget.previousDays = previousDays; + if (showTime) cleanedService.widget.showTime = showTime; } } diff --git a/src/utils/contexts/calendar.jsx b/src/utils/contexts/calendar.jsx deleted file mode 100644 index 578563a55..000000000 --- a/src/utils/contexts/calendar.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, useState, useMemo } from "react"; - -export const EventContext = createContext(); - -export function EventProvider({ initialEvent, children }) { - const [events, setEvents] = useState({}); - - if (initialEvent) { - setEvents(initialEvent); - } - - const value = useMemo(() => ({ events, setEvents }), [events]); - - return {children}; -} diff --git a/src/widgets/calendar/agenda.jsx b/src/widgets/calendar/agenda.jsx index 1ec4bb2e8..903592697 100644 --- a/src/widgets/calendar/agenda.jsx +++ b/src/widgets/calendar/agenda.jsx @@ -1,45 +1,11 @@ -import { useContext, useState } from "react"; import { DateTime } from "luxon"; import classNames from "classnames"; import { useTranslation } from "next-i18next"; -import { IoMdCheckmarkCircleOutline } from "react-icons/io"; -import { EventContext } from "../../utils/contexts/calendar"; +import Event from "./event"; -export function Event({ event, colorVariants, showDate = false }) { - const [hover, setHover] = useState(false); - const { i18n } = useTranslation(); - - return ( -
setHover(!hover)} - onMouseLeave={() => setHover(!hover)} - > - - - {showDate && - event.date.setLocale(i18n.language).startOf("day").toLocaleString({ month: "short", day: "numeric" })} - - - - - -
-
{hover && event.additional ? event.additional : event.title}
-
- {event.isCompleted && ( - - - - )} -
- ); -} - -export default function Agenda({ service, colorVariants, showDate }) { +export default function Agenda({ service, colorVariants, events, showDate }) { const { widget } = service; - const { events } = useContext(EventContext); const { t } = useTranslation(); if (!showDate) { @@ -59,10 +25,8 @@ export default function Agenda({ service, colorVariants, showDate }) { if (!eventsArray.length) { return (
-
-
+
+
eventsArray.filter((e) => e.date.startOf("day").ts === d)); return ( -
-
+
+
{eventsByDay.map((eventsDay, i) => (
{eventsDay.map((event, j) => ( ))}
diff --git a/src/widgets/calendar/component.jsx b/src/widgets/calendar/component.jsx index e99b97a75..d1ab2b4b8 100644 --- a/src/widgets/calendar/component.jsx +++ b/src/widgets/calendar/component.jsx @@ -40,6 +40,7 @@ export default function Component({ service }) { const { widget } = service; const { i18n } = useTranslation(); const [showDate, setShowDate] = useState(null); + const [events, setEvents] = useState({}); const currentDate = DateTime.now().setLocale(i18n.language).startOf("day"); const { settings } = useContext(SettingsContext); @@ -69,9 +70,9 @@ export default function Component({ service }) { ?.filter((integration) => integration?.type) .map((integration) => ({ service: dynamic(() => import(`./integrations/${integration.type}`)), - widget: integration, + widget: { ...widget, ...integration }, })) ?? [], - [widget.integrations], + [widget], ); return ( @@ -80,13 +81,14 @@ export default function Component({ service }) {
{integrations.map((integration) => { const Integration = integration.service; - const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group; + const key = `integration-${integration.widget.type}-${integration.widget.service_name}-${integration.widget.service_group}-${integration.widget.name}`; return ( @@ -95,8 +97,10 @@ export default function Component({ service }) {
{(!widget?.view || widget?.view === "monthly") && ( setHover(!hover)} + onMouseLeave={() => setHover(!hover)} + key={`event-${event.title}-${event.date}-${event.additional}`} + > + {showDateColumn && ( + + + {(showDate || showTime) && + event.date + .setLocale(i18n.language) + .toLocaleString(showTime ? DateTime.TIME_24_SIMPLE : { month: "short", day: "numeric" })} + + + )} + + + +
+
{hover && event.additional ? event.additional : event.title}
+
+ {event.isCompleted && ( + + + + )} +
+ ); +} diff --git a/src/widgets/calendar/integrations/ical.jsx b/src/widgets/calendar/integrations/ical.jsx new file mode 100644 index 000000000..b3f1f7818 --- /dev/null +++ b/src/widgets/calendar/integrations/ical.jsx @@ -0,0 +1,58 @@ +import { DateTime } from "luxon"; +import { parseString } from "cal-parser"; +import { useEffect } from "react"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "../../../utils/proxy/use-widget-api"; +import Error from "../../../components/services/widget/error"; + +export default function Integration({ config, params, setEvents, hideErrors }) { + const { t } = useTranslation(); + const { data: icalData, error: icalError } = useWidgetAPI(config, config.name, { + refreshInterval: 300000, // 5 minutes + }); + + useEffect(() => { + let parsedIcal; + + if (!icalError && icalData && !icalData.error) { + parsedIcal = parseString(icalData.data); + if (parsedIcal.events.length === 0) { + icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` }; + } + } + + if (icalError || !parsedIcal) { + return; + } + + const eventsToAdd = {}; + const events = parsedIcal?.getEventsBetweenDates( + DateTime.fromISO(params.start).toJSDate(), + DateTime.fromISO(params.end).toJSDate(), + ); + + events?.forEach((event) => { + let title = `${event?.summary?.value}`; + if (config?.params?.showName) { + title = `${config.name}: ${title}`; + } + + event.matchingDates.forEach((date) => { + eventsToAdd[event?.uid?.value] = { + title, + date: DateTime.fromJSDate(date), + color: config?.color ?? "zinc", + isCompleted: DateTime.fromJSDate(date) < DateTime.now(), + additional: event.location?.value, + type: "ical", + }; + }); + }); + + setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd })); + }, [icalData, icalError, config, params, setEvents, t]); + + const error = icalError ?? icalData?.error; + return error && !hideErrors && ; +} diff --git a/src/widgets/calendar/integrations/lidarr.jsx b/src/widgets/calendar/integrations/lidarr.jsx index 99ee77d7a..d4a6edbe2 100644 --- a/src/widgets/calendar/integrations/lidarr.jsx +++ b/src/widgets/calendar/integrations/lidarr.jsx @@ -1,12 +1,10 @@ import { DateTime } from "luxon"; -import { useContext, useEffect } from "react"; +import { useEffect } from "react"; import useWidgetAPI from "../../../utils/proxy/use-widget-api"; -import { EventContext } from "../../../utils/contexts/calendar"; import Error from "../../../components/services/widget/error"; -export default function Integration({ config, params, hideErrors = false }) { - const { setEvents } = useContext(EventContext); +export default function Integration({ config, params, setEvents, hideErrors = false }) { const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", { ...params, includeArtist: "false", diff --git a/src/widgets/calendar/integrations/radarr.jsx b/src/widgets/calendar/integrations/radarr.jsx index 22f6b1d39..945eadd9b 100644 --- a/src/widgets/calendar/integrations/radarr.jsx +++ b/src/widgets/calendar/integrations/radarr.jsx @@ -1,14 +1,12 @@ import { DateTime } from "luxon"; -import { useEffect, useContext } from "react"; +import { useEffect } from "react"; import { useTranslation } from "next-i18next"; import useWidgetAPI from "../../../utils/proxy/use-widget-api"; -import { EventContext } from "../../../utils/contexts/calendar"; import Error from "../../../components/services/widget/error"; -export default function Integration({ config, params, hideErrors = false }) { +export default function Integration({ config, params, setEvents, hideErrors = false }) { const { t } = useTranslation(); - const { setEvents } = useContext(EventContext); const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", { ...params, ...(config?.params ?? {}), diff --git a/src/widgets/calendar/integrations/readarr.jsx b/src/widgets/calendar/integrations/readarr.jsx index 8a9ba6b36..6ae919ef2 100644 --- a/src/widgets/calendar/integrations/readarr.jsx +++ b/src/widgets/calendar/integrations/readarr.jsx @@ -1,12 +1,10 @@ import { DateTime } from "luxon"; -import { useEffect, useContext } from "react"; +import { useEffect } from "react"; import useWidgetAPI from "../../../utils/proxy/use-widget-api"; -import { EventContext } from "../../../utils/contexts/calendar"; import Error from "../../../components/services/widget/error"; -export default function Integration({ config, params, hideErrors = false }) { - const { setEvents } = useContext(EventContext); +export default function Integration({ config, params, setEvents, hideErrors = false }) { const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", { ...params, includeAuthor: "true", diff --git a/src/widgets/calendar/integrations/sonarr.jsx b/src/widgets/calendar/integrations/sonarr.jsx index d308ec6f2..34cc679dd 100644 --- a/src/widgets/calendar/integrations/sonarr.jsx +++ b/src/widgets/calendar/integrations/sonarr.jsx @@ -1,12 +1,10 @@ import { DateTime } from "luxon"; -import { useEffect, useContext } from "react"; +import { useEffect } from "react"; import useWidgetAPI from "../../../utils/proxy/use-widget-api"; -import { EventContext } from "../../../utils/contexts/calendar"; import Error from "../../../components/services/widget/error"; -export default function Integration({ config, params, hideErrors = false }) { - const { setEvents } = useContext(EventContext); +export default function Integration({ config, params, setEvents, hideErrors = false }) { const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", { ...params, includeSeries: "true", diff --git a/src/widgets/calendar/monthly.jsx b/src/widgets/calendar/monthly.jsx index e3c9e1fd4..8a208bc1b 100644 --- a/src/widgets/calendar/monthly.jsx +++ b/src/widgets/calendar/monthly.jsx @@ -1,10 +1,9 @@ -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; import { DateTime, Info } from "luxon"; import classNames from "classnames"; import { useTranslation } from "next-i18next"; -import { IoMdCheckmarkCircleOutline } from "react-icons/io"; -import { EventContext } from "../../utils/contexts/calendar"; +import Event from "./event"; const cellStyle = "relative w-10 flex items-center justify-center flex-col"; const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer"; @@ -32,11 +31,11 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS // selected same day style style += - displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy") + displayDate.startOf("day").ts === showDate.startOf("day").ts ? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md " : ""; - if (displayDate.toFormat("MM-dd-yyyy") === currentDate.toFormat("MM-dd-yyyy")) { + if (displayDate.startOf("day").ts === currentDate.startOf("day").ts) { // today style style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md "; } else { @@ -61,7 +60,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS .slice(0, 4) .map((event) => ( ))} @@ -70,25 +69,6 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS ); } -export function Event({ event }) { - return ( -
- - {event.title} - {event.additional ? ` - ${event.additional}` : ""} - - {event.isCompleted && ( - - - - )} -
- ); -} - const dayInWeekId = { monday: 1, tuesday: 2, @@ -99,10 +79,9 @@ const dayInWeekId = { sunday: 7, }; -export default function Monthly({ service, colorVariants, showDate, setShowDate }) { +export default function Monthly({ service, colorVariants, events, showDate, setShowDate }) { const { widget } = service; const { i18n } = useTranslation(); - const { events } = useContext(EventContext); const currentDate = DateTime.now().setLocale(i18n.language).startOf("day"); const dayNames = Info.weekdays("short", { locale: i18n.language }); @@ -161,7 +140,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
-
+
{dayNames.map((name) => ( @@ -172,7 +151,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
@@ -191,12 +170,18 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate )}
-
+
{eventsArray - ?.filter((event) => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger()) + ?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts) .slice(0, widget?.maxEvents ?? 10) .map((event) => ( - + ))}
diff --git a/src/widgets/calendar/proxy.js b/src/widgets/calendar/proxy.js new file mode 100644 index 000000000..996ea3241 --- /dev/null +++ b/src/widgets/calendar/proxy.js @@ -0,0 +1,33 @@ +import getServiceWidget from "utils/config/service-helpers"; +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; + +const logger = createLogger("calendarProxyHandler"); + +export default async function calendarProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + const integration = widget.integrations?.find((i) => i.name === endpoint); + + if (integration) { + if (!integration.url) { + return res.status(403).json({ error: "No integration URL specified" }); + } + + const [status, contentType, data] = await httpProxy(integration.url); + + if (contentType) res.setHeader("Content-Type", contentType); + + if (status !== 200) { + logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`); + return res.status(status).send(data); + } + + return res.status(status).json({ data: data.toString() }); + } + } + + return res.status(400).json({ error: "Invalid integration" }); +} diff --git a/src/widgets/calendar/widget.js b/src/widgets/calendar/widget.js new file mode 100644 index 000000000..ef382f378 --- /dev/null +++ b/src/widgets/calendar/widget.js @@ -0,0 +1,8 @@ +import calendarProxyHandler from "./proxy"; + +const widget = { + api: "{url}", + proxyHandler: calendarProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 6bc682dad..0a2d24ed0 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -6,6 +6,7 @@ import autobrr from "./autobrr/widget"; import azuredevops from "./azuredevops/widget"; import bazarr from "./bazarr/widget"; import caddy from "./caddy/widget"; +import calendar from "./calendar/widget"; import calibreweb from "./calibreweb/widget"; import changedetectionio from "./changedetectionio/widget"; import channelsdvrserver from "./channelsdvrserver/widget"; @@ -131,6 +132,7 @@ const widgets = { homeassistant, homebridge, healthchecks, + ical: calendar, immich, jackett, jdownloader,