2023-11-25 16:17:25 +00:00
|
|
|
import { parseString } from "cal-parser";
|
2025-03-30 21:40:03 -07:00
|
|
|
import { DateTime } from "luxon";
|
2023-11-25 16:17:25 +00:00
|
|
|
import { useTranslation } from "next-i18next";
|
2025-03-30 21:40:03 -07:00
|
|
|
import { useEffect } from "react";
|
2023-12-10 15:19:43 +00:00
|
|
|
import { RRule } from "rrule";
|
2023-11-25 16:17:25 +00:00
|
|
|
|
|
|
|
import Error from "../../../components/services/widget/error";
|
2025-03-30 21:40:03 -07:00
|
|
|
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
2023-11-25 16:17:25 +00:00
|
|
|
|
2024-01-26 01:14:43 -08:00
|
|
|
// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
|
|
|
|
function simpleHash(str) {
|
|
|
|
/* eslint-disable no-plusplus, no-bitwise */
|
|
|
|
let hash = 0;
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
|
|
|
|
}
|
|
|
|
return (hash >>> 0).toString(36);
|
|
|
|
/* eslint-disable no-plusplus, no-bitwise */
|
|
|
|
}
|
|
|
|
|
2024-01-17 23:00:51 +00:00
|
|
|
export default function Integration({ config, params, setEvents, hideErrors, timezone }) {
|
2023-11-25 16:17:25 +00:00
|
|
|
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")}` };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-17 23:00:51 +00:00
|
|
|
const startDate = DateTime.fromISO(params.start);
|
|
|
|
const endDate = DateTime.fromISO(params.end);
|
2023-12-10 15:19:43 +00:00
|
|
|
|
|
|
|
if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
|
2023-11-25 16:17:25 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const eventsToAdd = {};
|
2023-12-10 15:19:43 +00:00
|
|
|
const events = parsedIcal?.getEventsBetweenDates(startDate.toJSDate(), endDate.toJSDate());
|
2024-01-17 23:00:51 +00:00
|
|
|
const now = timezone ? DateTime.now().setZone(timezone) : DateTime.now();
|
2023-11-25 16:17:25 +00:00
|
|
|
|
|
|
|
events?.forEach((event) => {
|
|
|
|
let title = `${event?.summary?.value}`;
|
|
|
|
if (config?.params?.showName) {
|
|
|
|
title = `${config.name}: ${title}`;
|
|
|
|
}
|
|
|
|
|
2025-02-20 11:29:43 -08:00
|
|
|
// 'dtend' is null for all-day events
|
|
|
|
const { dtstart, dtend = { value: 0 } } = event;
|
|
|
|
|
2023-12-10 15:19:43 +00:00
|
|
|
const eventToAdd = (date, i, type) => {
|
2024-02-11 05:30:37 +09:00
|
|
|
const days = dtend.value === 0 ? 1 : (dtend.value - dtstart.value) / (1000 * 60 * 60 * 24);
|
2024-01-17 23:00:51 +00:00
|
|
|
const eventDate = timezone ? DateTime.fromJSDate(date, { zone: timezone }) : DateTime.fromJSDate(date);
|
2024-01-15 02:01:10 +00:00
|
|
|
|
2023-12-10 15:19:43 +00:00
|
|
|
for (let j = 0; j < days; j += 1) {
|
2024-01-26 01:14:43 -08:00
|
|
|
// See https://github.com/gethomepage/homepage/issues/2753 uid is not stable
|
|
|
|
// assumption is that the event is the same if the start, end and title are all the same
|
2024-02-11 05:30:37 +09:00
|
|
|
const hash = simpleHash(`${dtstart?.value}${dtend?.value}${title}${i}${j}${type}}`);
|
2024-01-26 01:14:43 -08:00
|
|
|
eventsToAdd[hash] = {
|
2023-12-10 15:19:43 +00:00
|
|
|
title,
|
2024-01-15 02:01:10 +00:00
|
|
|
date: eventDate.plus({ days: j }),
|
2023-12-10 15:19:43 +00:00
|
|
|
color: config?.color ?? "zinc",
|
2024-01-15 02:01:10 +00:00
|
|
|
isCompleted: eventDate < now,
|
2023-12-10 15:19:43 +00:00
|
|
|
additional: event.location?.value,
|
|
|
|
type: "ical",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-02-20 11:29:43 -08:00
|
|
|
let recurrenceOptions = event?.recurrenceRule?.origOptions;
|
|
|
|
// RRuleSet does not have dtstart, add it manually
|
|
|
|
if (event?.recurrenceRule && event.recurrenceRule.rrules && event.recurrenceRule.rrules()?.[0]?.origOptions) {
|
|
|
|
recurrenceOptions = event.recurrenceRule.rrules()[0].origOptions;
|
|
|
|
recurrenceOptions.dtstart = dtstart.value;
|
|
|
|
}
|
|
|
|
|
2024-01-14 21:49:28 +00:00
|
|
|
if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
|
2024-01-20 00:15:45 -08:00
|
|
|
try {
|
|
|
|
const rule = new RRule(recurrenceOptions);
|
|
|
|
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
|
|
|
|
|
2025-02-12 07:38:36 -08:00
|
|
|
recurringEvents.forEach((date, i) => {
|
|
|
|
let eventDate = date;
|
|
|
|
if (event.dtstart?.params?.tzid) {
|
|
|
|
// date is in UTC but parsed as if it is in current timezone, so we need to adjust it
|
|
|
|
const dateInUTC = DateTime.fromJSDate(date).setZone("UTC");
|
|
|
|
const offset = dateInUTC.offset - DateTime.fromJSDate(date, { zone: event.dtstart.params.tzid }).offset;
|
|
|
|
eventDate = dateInUTC.plus({ minutes: offset }).toJSDate();
|
|
|
|
}
|
|
|
|
eventToAdd(eventDate, i, "recurring");
|
|
|
|
});
|
2024-01-20 00:15:45 -08:00
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.error("Unable to parse recurring events from iCal: %s", e);
|
|
|
|
}
|
2023-12-10 15:19:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
event.matchingDates.forEach((date, i) => eventToAdd(date, i, "single"));
|
2023-11-25 16:17:25 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
2024-01-17 23:00:51 +00:00
|
|
|
}, [icalData, icalError, config, params, setEvents, timezone, t]);
|
2023-11-25 16:17:25 +00:00
|
|
|
|
|
|
|
const error = icalError ?? icalData?.error;
|
|
|
|
return error && !hideErrors && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
|
|
|
}
|